線程安全與volatile關鍵字

volatile關鍵字,語義有二:html

  1. volatile修飾的變量對於其餘線程具備當即可見性
  2. 禁止指令重排序

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

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+一、將結果同步回主內存。那麼就有可能一個線程拿到值,正開始執行i+1,而值還將來得及改變時,另外一個線程也一樣正在進行i+1。這樣一來,就有可能兩個線程給同一個值加了一次1,因此就算有volatile修飾也是無力迴天。安全

這時,咱們應該使用synchronize或concurrent原子類來保證「操做」的原子性。固然「一寫多讀」是線程安全的,由於不涉及到多個線程來「寫」,致使的值重複寫入問題。故volatile的使用場景應該是:修飾的變量的有關操做都是原子性的時候。好比修飾一個控制標誌位:併發

volatile boolean tag=true;

線程1 while(tag){};

線程2 while(tag){};
複製代碼

當tag=false時,兩個線程都能立刻感知到並中止while循環,由於簡單的賦值語句屬於原子操做(請注意是:賦予具體的值而不是變量),它只負責把主內存的tag同步爲true。this

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

  • synchronize是由於變量執行解鎖操做前,會把變量同步到主內存(自帶可見性);spa

  • 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高級特性與最佳實踐》

相關文章
相關標籤/搜索