1. 程式人生 > >(四)多執行緒說學逗唱:執行緒險惡,變數和執行緒安全不得不防

(四)多執行緒說學逗唱:執行緒險惡,變數和執行緒安全不得不防

不共享資料與共享資料
  • 在多執行緒環境中,例項變數針對其他執行緒存在共享和不共享之分,他們的情況如下圖:
    不共享資料與不共享資料
  • 首先來看一看不共享資料,它的特點就是存在多個數據源,每一個執行緒都使用針對的資料,執行緒之間的佔用的資源不會互相影響,反觀共享資料,只有一個數據源,多個執行緒之間均使用的是同一個資料。在接下來的程式碼示例中,我將會用兩個例子向大家展示這兩種資料形式的差別。
package day01;
/**
 * 
* @ClassName: NoShareInstanceVariable
* @Description: 不共享例項變數學習
* @author Jian.Wang
* @date 2018年12月6日
*
 */
public class NoShareInstanceVariable {

	public static void main(String[] args) {
		try {
			NoShareVariable noShareVariable_a = new NoShareVariable("A");
			NoShareVariable noShareVariable_b = new NoShareVariable("B");
			NoShareVariable noShareVariable_c = new NoShareVariable("C");
			noShareVariable_a.start();
			noShareVariable_b.start();
			noShareVariable_c.start();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

/**
 * 
* @ClassName: NoShareVariable
* @Description: 不共享例項變數測試類
* @author Jian.Wang
* @date 2018年12月6日
*
* synchronized 標識為“互斥區”或“臨界區”,區中的程式碼意思上為加上了鎖,任何執行緒訪問臨界區的程式碼都需要先拿到鎖.....
 */
class NoShareVariable extends Thread{
	private int count = 5;
	public NoShareVariable(String name) {
		super();
		this.setName(name);
	}
	@SuppressWarnings("static-access")
	@Override
	public void run() {
		while (count > 0) {
			count--;
			System.out.println("由:" + this.currentThread().getName() + "計算,count=" + count);
		}
	}
}

  • NoShareVariable 類繼承了Thread類,重寫了run方法,無疑他是一個執行緒類物件,在run()方法中對於count每一次迴圈減一,在輸出當前的值。NoShareInstanceVariable 類中建立了三個NoShareVariable物件的實體,並且開啟了三個執行緒。很明顯這三個執行緒都有各自的count變數,自己減少自己的count值,這樣的情況就是變數不共享,各自為政,自立山頭,他的輸出結果為:
    在這裡插入圖片描述
package day01;
/**
 * 
* @ClassName: NoShareInstanceVariable
* @Description: 不共享例項變數學習
* @author Jian.Wang
* @date 2018年12月6日
*
 */
public class NoShareInstanceVariable {

	public static void main(String[] args) {
		try {
			ShareVariable shareVariable = new ShareVariable();
			Thread shareVariable_a = new Thread(shareVariable,"A");
			Thread shareVariable_b = new Thread(shareVariable,"B");
			Thread shareVariable_c = new Thread(shareVariable,"C");
			Thread shareVariable_d = new Thread(shareVariable,"D");
			Thread shareVariable_e = new Thread(shareVariable,"E");
			Thread shareVariable_f = new Thread(shareVariable,"F");
			shareVariable_a.start();
			shareVariable_b.start();
			shareVariable_c.start();
			shareVariable_d.start();
			shareVariable_e.start();
			shareVariable_f.start();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

/**
 * 
* @ClassName: ShareVariable
* @Description: 共享例項變數類
* @author Jian.Wang
* @date 2018年12月6日
*
 */
class ShareVariable extends Thread{
	private int count = 5;
	@SuppressWarnings("static-access")
	@Override
	 public void run() {
		super.run();
		count--;
		System.out.println("由:" + this.currentThread().getName() + "計算,count=" + count);
	}	
}

  • ShareVariable類中定義了一個私有變數count,在run方法中不斷自減,在NoShareInstanceVariable 中建立了一個實體,並作為引數傳遞到Thread中,建立了若干個執行緒。此時count資料就被建立的所有執行緒共享,其最後編碼的結果為:
    在這裡插入圖片描述
  • 上面的輸出結果分別遞減,也就是按照猜想所有執行緒共享一個count。但是細心的朋友會發現,F和E執行緒都訪問了-1這個值,這又是為什麼呢?原因就是CPU的速度是非常快的,這兩個執行緒在幾乎同一個時間訪問了count這個值,在某一個執行緒還沒有執行減操作是就也進行了訪問,因此造成了同時輸出兩個一樣的值,那麼這種情況該怎麼解決呢?這就要用到我們接下來就要講到的東西,處理執行緒安全問題。
synchronized處理執行緒安全
  • 上面的情況要解決,只需要在run方法前加上synchronized關鍵字即可。
    在這裡插入圖片描述
  • 通過run方法前加synchronized關鍵字,使得多個執行緒在執行run方法的時候,以排隊的方式進行處理。當一個執行緒呼叫run方法前,先判斷run方法有沒有被上鎖,如果上鎖,說明有其他的執行緒正在呼叫run方法,必須等待其他執行緒對run方法地哦啊用結束以後才可以執行run方法。這樣也就實現了排隊呼叫run方法的目的。synchronized可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為“互斥區”或“臨界區”。當一個執行緒想要執行同步方法中的程式碼時,執行緒必須首先嚐試拿到這把鎖,如果能夠拿到這把鎖就能夠執行synchronized裡面的程式碼。如果拿不到這把鎖,那麼執行緒就會不斷地嘗試拿這把鎖,知道拿到為止,而且是同時有多個執行緒同時去爭搶這把鎖。下面就通過一個簡單的例子來展示這一場景,寫一個LoginServlet類,建立doPost方法模擬登陸業務。A和B兩個登入員分別為兩個不同的執行緒,呼叫doPost方法,看看在非執行緒安全和執行緒安全下展示出來不同的現象。
package day01;

import java.util.Objects;
/**
 * 
* @ClassName: NonThreadSafe
* @Description: 模擬非執行緒安全,並解決
* @author Jian.Wang
* @date 2018年12月6日
*
 */
public class NonThreadSafe {

	public static void main(String[] args) {
		ALogin aLogin = new ALogin();
		aLogin.start();
		BLogin bLogin = new BLogin();
		bLogin.start();
	}
}

/**
 * 
* @ClassName: LoginServlet
* @Description: 模擬登陸類,包含登陸方法
* @author Jian.Wang
* @date 2018年12月6日
*
 */
class LoginServlet {
	private static String userNameRef;
	private static String passwordRef;
	
	public static void doPost(String userName, String password) {
		try {
			userNameRef = userName;
			if (Objects.equals(userName, "a")) {				
				Thread.sleep(3000);
			}
			passwordRef = password;
			System.out.println("userName=" + userNameRef + ", password=" + passwordRef);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

/**
 * 
* @ClassName: ALogin
* @Description: A登陸員
* @author Jian.Wang
* @date 2018年12月6日
*
 */
class ALogin extends Thread {

	@Override
	public void run() {
		super.run();
		LoginServlet.doPost("a", "aa");
	}
}

/**
 * 
* @ClassName: BLogin
* @Description: B登陸員
* @author Jian.Wang
* @date 2018年12月6日
*
 */
class BLogin extends Thread {

	@Override
	public void run() {
		super.run();
		LoginServlet.doPost("b", "bb");
	}
	
}

  • doPost方法前沒有加入synchronized 關鍵字時,出現了結果混亂的情況。
    在這裡插入圖片描述
  • 因此我們將synchronized 加到doPost方法上之後,結果就正確了。
    在這裡插入圖片描述
多說一句 synchronized 在很多常用類中基本都用到了
  • 資料安全是應用程式中的重中之重,因此在實際的開發過程中,往往對於重要資料的安全,特別是共享資料的安全維護是必然的,因此一些提供的常用類中很多方法都是執行緒安全的,比如StringBuffer等,雖然執行效率低點,但是能夠保護資料,同志們在平時的學習過程中還是要多多注意,萬一在面試的時候被面試官問到了答不出來豈不是很尷尬…