volatile的理解

存在背景

緩存不一致性問題:在多核CPU中,每條線程可能運行於不一樣的CPU中,所以每一個線程運行時有本身的高速緩存(對單核CPU來講,其實也會出現這種問題,只不過是以線程調度的形式來分別執行的)。c++

解決方法:緩存

  • 經過在總線加LOCK#鎖的方式  
  • 經過緩存一致性協議  

CPU和其餘部件進行通訊都是經過總線來進行的,對總線加LOCK#鎖,阻塞了其餘CPU對其餘部件訪問(如內存),只能有一個CPU能使用這個變量的內存。併發

最出名的就是Intel的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。優化

用法解釋

一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:spa

  1)保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。線程

  2)禁止進行指令重排序(他上面的比他先執行,他下面的比他後執行,但並不保證上面和下面的代碼塊也是有序執行)code

保證可見性:對象

  volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。並且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任什麼時候刻,兩個不一樣的線程老是看到某個成員變量的同一個值。排序

不保證原子性:內存

  譬如inc++,自增操做是不具有原子性的,它包括讀取變量的原始值、進行加1操做、寫入工做內存。那麼就是說自增操做的三個子操做可能會分割開執行,就有可能致使下面這種狀況出現:

  假如某個時刻變量inc的值爲10,

  線程1對變量進行自增操做,線程1先讀取了變量inc的原始值,而後線程1被阻塞了;

  而後線程2對變量進行自增操做,線程2也去讀取變量inc的原始值,因爲線程1只是對變量inc進行讀取操做,而沒有對變量進行修改操做,因此不會致使線程2的工做內存中緩存變量inc的緩存行無效,因此線程2會直接去主存讀取inc的值,發現inc的值時10,而後進行加1操做,並把11寫入工做內存,最後寫入主存。

  而後線程1接着進行加1操做,因爲已經讀取了inc的值,注意此時在線程1的工做內存中inc的值仍然爲10,因此線程1對inc進行加1操做後inc的值爲11,而後將11寫入工做內存,最後寫入主存。

  那麼兩個線程分別進行了一次自增操做後,inc只增長了1。

  解釋到這裏,前面保證的是一個變量在修改volatile變量時,會讓緩存行無效,而後其餘線程去讀就會讀到新的值。可是要注意,線程1對變量進行讀取操做以後被阻塞了,並無對inc值進行修改。而後雖然volatile能保證線程2對變量inc的值讀取是從內存中讀取的,可是線程1沒有進行修改,因此線程2根本就不會看到修改的值。根源就在這裏,自增操做不是原子性操做,並且volatile也沒法保證對變量的任何操做都是原子性的。

必定程度上保證有序性:

  volatile關鍵字禁止指令重排序有兩層意思:

  1)當程序執行到volatile變量的讀操做或者寫操做時,在其前面的操做的更改確定所有已經進行,且結果已經對後面的操做可見;在其後面的操做確定尚未進行;

  2)在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

原理解釋

JVM角度:

  Java使用一個主內存來保存變量當前值,而每一個線程則有其獨立的工做內存。

  線程訪問變量的時候會將變量的值拷貝到本身的工做內存中,這樣,當線程對本身工做內存中的變量進行操做以後,就形成了工做內存中的變量拷貝的值與主內存中的變量值不一樣

  Java語言規範中指出:爲了得到最佳速度,容許線程保存共享成員變量的私有拷貝,並且只當線程進入或者離開同步代碼塊時才與共享成員變量的原始值對比。

  這樣當多個線程同時與某個對象交互時,就必需要注意到要讓線程及時的獲得共享成員變量的變化。

  而volatile關鍵字就是提示VM:對於這個成員變量不能保存它的私有拷貝,會使在工做內存中的私有拷貝失效,從而再去主內存取值。

彙編角度:

  「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」

  lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:

  1)它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;

  2)它會強制將對緩存的修改操做當即寫入主存;

  3)若是是寫操做,它會致使其餘CPU中對應的L1&L2緩存行無效。

使用條件:

一般來講,使用volatile必須具有如下2個條件:

  1)對變量的寫操做不依賴於當前值

  2)該變量沒有包含在具備其餘變量的不變式中

實際上,這些條件代表,能夠被寫入 volatile 變量的這些有效值獨立於任何程序的狀態,包括變量的當前狀態。

事實上,個人理解就是上面的2個條件須要保證操做是原子性操做,才能保證使用volatile關鍵字的程序在併發時可以正確執行。

 

下面列舉幾個Java中使用volatile的幾個場景。

1.狀態標記量

volatile   boolean   flag =   false  ;
 //!flag是原子性操做,修改flag後便可見
while  (!flag){
      doSomething();
}
 
public   void   setFlag() {
      flag =   true  ;

    }

volatile   boolean   inited =   false  ;
//線程1:
context = loadContext();  
//保證運行 inited =   true  ;以前已被初始化,用到了必定程度的有序性
inited =   true  ;            
 
//線程2:
while  (!inited ){
sleep()
}
doSomethingwithconfig(context);
相關文章
相關標籤/搜索