1. 程式人生 > >執行緒安全與volatile關鍵字

執行緒安全與volatile關鍵字

volatile關鍵字,語義有二:

  1. volatile修飾的變數對於其他執行緒具有立即可見性
  2. 禁止指令重排序

下面進行詳細介紹,並聊聊Java先行發生原則與volatile。

volatile修飾的變數對於其他執行緒具有立即可見性

即被volatile修飾的變數值發生變化時,其他執行緒可以立馬感知。而對於普通變數,值發生變化後,需要經過store、write過程將變數從當前執行緒的工作記憶體寫入主記憶體,其他執行緒再從主記憶體通過read、load將變數同步到自己的工作記憶體,由於以上流程時間上的影響,可能會導致執行緒的不安全。

當然要說使用volatile修飾過的變數是執行緒安全的,也不全對。因為volatile是要分場景來說的:如果多個執行緒操作volatile修飾的變數,且此時的“操作”是原子性的,那麼是執行緒安全的,否則不是。如:

volatile int i=0;

執行緒1執行: for(;i++;i<100);

執行緒2執行: for(;i++;i<100); 
複製程式碼

最後 i 的結果不一定會是200(即執行緒不安全),因為i++操作不是原子性操作,它涉及到了三個子操作:從主記憶體取出i、i+1、將結果同步回主記憶體。那麼就有可能一個執行緒拿到值,正開始執行i+1,而值還未來得及改變時,另一個執行緒也同樣正在進行i+1。這樣一來,就有可能兩個執行緒給同一個值加了一次1,所以就算有volatile修飾也是無力迴天。

這時,我們應該使用synchronize或concurrent原子類來保證“操作”的原子性。當然“一寫多讀

”是執行緒安全的,因為不涉及到多個執行緒來“寫”,導致的值重複寫入問題。故volatile的使用場景應該是:修飾的變數的有關操作都是原子性的時候。比如修飾一個控制標誌位:

volatile boolean tag=true;

執行緒1 while(tag){};

執行緒2 while(tag){};
複製程式碼

當tag=false時,兩個執行緒都能馬上感知到並停止while迴圈,因為簡單的賦值語句屬於原子操作(請注意是:賦予具體的值而不是變數),它只負責把主記憶體的tag同步為true。

能實現可見性的關鍵字除了volatile,還有synchronize與final:

  • synchronize是因為變數執行解鎖操作前,會把變數同步到主記憶體(自帶可見性);

  • final則是被其修飾的變數一旦初始化,且構造器沒有把this引用傳遞到外面去的情況下,其他執行緒就可以看見它的值(因為它永不發生變化)。

禁止指令重排序

new一個物件可以分解為如下的3行虛擬碼

memory=allocate(); //1:分配物件的記憶體空間

ctorInstance(memory); //2:初始化物件

instance=memory; //3:設定instance指向剛分配的記憶體地址
複製程式碼

上面三行程式碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的)。2和3之間重排序之後的執行順序可能如下:

memory=allocate(); //1:分配物件的記憶體空間

instance=memory; //3:設定instance指向剛分配的記憶體地址,注意此時物件還沒有被初始化

ctorInstance(memory); //2:初始化物件
複製程式碼

如果發生重排序,另一個併發執行的執行緒B就有可能在還沒初始化物件操作前就拿走了instance,但此時這個物件可能還沒有被執行緒真正初始化,因此這是執行緒不安全的。

“Java先行發生原則”與valatile

Java先行發生原則:

  1. 程式次序規則:在一個執行緒內,按照程式程式碼順序(準確說應是控制流順序),先寫的先發生,後寫的後發生。

  2. 管程鎖定規則:一個解鎖操作先於後面對該鎖的鎖定操作。

  3. volatile變數規則:對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作。

  4. 執行緒啟動規則:執行緒物件的start()方法先行發生於此執行緒的每一個動作。

  5. 執行緒終止規則:執行緒的所有操作都先於此執行緒的終止檢測。

  6. 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生。可通過Thread.interrupted()方法檢測是否會有中斷將發生。

  7. 物件終結規則:一個物件的初始化發生先行於它的finalize()方法的開始。

  8. 傳遞性:如果操作A先於B,B先於C,那麼A先於C。

例:

private int value=0;

public void setValue(int value){
	this.value=value; 
}

public int getValue(){
	return this.value;
}
複製程式碼

如果執行緒1呼叫setValue(1)方法,執行緒2呼叫getValue(),那麼得到的value是0還是1呢,這是不確定的。因為它不滿足上面的先行發生原則:

  • 因為不是在一個執行緒,所以不符合程式次序規則
  • 因為沒有同步塊,也就不存在加鎖和解鎖,因此也不符合管程鎖定規則
  • 沒有volatile修飾,也就不存在volatile變數規則
  • 當然更沒有後面的執行緒相關規則和傳遞性可言。

針對此,可做以下修改:

  • 將上面的setter、getter方法都用synchronize修飾,使其滿足管程鎖定規則;
  • 使用volatile修飾,因為setValue()是基本的賦值操作,屬於原子操作,因此符合volatile的使用場景。

總結

  1. 執行緒安全一般至少需要兩個特性:原子性和可見性。

  2. synchronize是具有原子性和可見性的,所以如果使用了synchronize修飾的操作,那麼就自帶了可見性,也就不再需要volatile來保證可見性了。

  3. 若想實現執行緒安全的數字的自增自減等操作,也可使用java.util.concurrent.atomic包來進行無鎖的原子性操作。在其底層實現中,如AtomicInteger,同樣是:

    • 使用了volatile來保證可見性

    • 使用Unsafe呼叫native本地方法CAS,CAS採用匯流排加鎖或快取加鎖方式來保證原子性。

參考:

(Java併發程式設計:volatile關鍵字解析)www.importnew.com/18126.html

《深入理解Java虛擬機器:JVM高階特性與最佳實踐》