Volatile的詳解

volatile關鍵字修飾的共享變量主要有兩個特色:1.保證了不一樣線程訪問的內存可見性    2.禁止重排序java

在說內存可見性和有序性以前,咱們有必要看一下Java的內存模型(注意和JVM內存模型的區分)c++

爲何要有java內存模型?緩存

首先咱們知道內存訪問和CPU指令在執行速度上相差很是大,徹底不是一個數量級,爲了使得java在各個平臺上運行的差距減小,哪些搞處理器的大佬就在CPU上加了各類高速緩存,來減小內存操做和CPU指令的執行速度差距。而Java在java層面又進行了一波抽象,java內存模型將內存分爲工做內存和主存,每一個線程從主存load數據到工做內存,將load的數據賦值給工做內存上的變量,而後該工做內存對應的線程進行處理,處理結果在賦值給其工做內存,而後再將數據賦值給主存中的變量(這時候須要有一張圖)。多線程

使用工做內存和主存雖然加快了處理速度,可是也帶來了一些問題,好比下面這個例子併發

1         int i = 1;
2         i = i+1;

當在單線程狀況下,i最後的值必定是2;可是在兩個線程狀況下必定是3嗎?那就未必了。當線程A讀取i的值爲1,load到其工做內存,這時CPU切換至線程B,線程B讀取i的值也是1,而後對加1而後save到主存,這時線程A也對i進行加1,也save回主存,但最終i的值爲2。若是寫操做比較慢,你讀到的值還有多是1,這就是緩存不一致的問題。JMM就是圍繞着原子性,內存可見性,有序性這三個特徵創建的。經過解決這個三個特徵來解決緩存不一致的問題。而volatile主要針對於內存可見性和有序性。app

原子性性能

原子性是指一個操做要麼成功,那麼失敗,沒有中間狀態,好比i=1,直接讀取i的值,這確定是原子操做;可是i++,看似好像是,其實須要先讀取i的值,而後+1,最後在賦值給i,須要三個步驟,這就不是原子性操做。在JDK1.5引入了boolean、long、int對應的原子性類AtomicBoolean、AtomicLong、AtomicInteger,他們能夠提供原子性操做。atom

內存可見性spa

具備內存可見性的變量在被線程修改之後,會馬上刷新到主存並使其餘線程的緩存行上的數據失效線程

volatile修飾的變量具備內存可見性,主要表現爲:當寫一個volatile變量時,JMM會將該線程對應的工做內存中的共享變量當即刷新到主存;當讀一個volatile變量時,JMM會把該線程對應的工做內存中的值置爲無效,而後從主存中進行讀取,可是若是沒有線程對該共享變量進行修改,則不會觸發該操做。

有序性

JMM是容許處理器和編譯器對指令進行重排序的,但規定了as-if-serial,即不管怎麼重排序,最終結果都是同樣的。好比下面這段代碼:

1         int weight = 10;                           //A
2         int high = 5;                                //B
3         int area = high * weight * high;    //C

這段代碼中能夠按照A-->B-->C執行,也能夠按照B-->A-->C執行,由於A和B是相互獨立的,而C依賴於A、B,因此C不能排到A或B的前面。JMM保證了單線程的重排序,可是在多線程中就容易出現問題。好比下面這種狀況

 1 boolean flag = false;
 2     int a = 0;
 3     
 4     public void write(){
 5         int a = 2;                //1
 6         flag = true;              //2
 7     }
 8     public void multiply(){
 9         if(flag){                //3
10             int ret = a * a ;    //4
11         }
12     }

若是有兩個線程執行上面的代碼,線程1先執行write方法,隨後線程2執行multiply方法。最後結果必定是4嗎,不必定。

如圖,JMM對1和2進行了重排序,先將flag設置爲true,這是線程2執行,因爲a尚未賦值,因此最後ret的值爲0;

若是使用volatile關鍵字修飾flag,禁止重排序,能夠保證程序的有序性,也可使用synchronized或者lock這種重量級鎖來保證有序性,但性能會降低。

另外,JMM具有一些先天的有序性,即不須要經過任何手段就能夠保證的有序性,一般稱爲happens-before原則。<<JSR-133:Java Memory Model and Thread Specification>>定義了以下happens-before規則:

  1. 程序順序規則: 一個線程中的每一個操做,happens-before於該線程中的任意後續操做

  2. 監視器鎖規則:對一個線程的解鎖,happens-before於隨後對這個線程的加鎖

  3. volatile變量規則: 對一個volatile域的寫,happens-before於後續對這個volatile域的讀

  4. 傳遞性:若是A happens-before B ,且 B happens-before C, 那麼 A happens-before C

  5. start()規則: 若是線程A執行操做ThreadB_start()(啓動線程B) , 那麼A線程的ThreadB_start()happens-before 於B中的任意操做

  6. join()原則: 若是A執行ThreadB.join()而且成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。

  7. interrupt()原則: 對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測是否有中斷髮生

  8. finalize()原則:一個對象的初始化完成先行發生於它的finalize()方法的開始

第1條規則程序順序規則是說在一個線程裏,全部的操做都是按順序的,可是在JMM裏其實只要執行結果同樣,是容許重排序的,這邊的happens-before強調的重點也是單線程執行結果的正確性,可是沒法保證多線程也是如此。

第2條規則監視器規則其實也好理解,就是在加鎖以前,肯定這個鎖以前已經被釋放了,才能繼續加鎖。

第3條規則,就適用到所討論的volatile,若是一個線程先去寫一個變量,另一個線程再去讀,那麼寫入操做必定在讀操做以前。

第4條規則,就是happens-before的傳遞性。

 

須要注意的是,被volatile修飾的共享變量只知足內存可見性和禁止重排序,並不能保證原子性。好比volatile i++。

 1 public class Test {
 2     public volatile int inc = 0;
 3  
 4     public void increase() {
 5         inc++;
 6     }
 7  
 8     public static void main(String[] args) {
 9         final Test test = new Test();
10         for(int i=0;i<10;i++){
11             new Thread(){
12                 public void run() {
13                     for(int j=0;j<1000;j++)
14                         test.increase();
15                 };
16             }.start();
17         }
18  
19         while(Thread.activeCount()>1)  //保證前面的線程都執行完
20             Thread.yield();
21         System.out.println(test.inc);
22     }

按道理來講結果是10000,可是運行下極可能是個小於10000的值。有人可能會說volatile不是保證了可見性啊,一個線程對inc的修改,另一個線程應該馬上看到啊!但是這裏的操做inc++是個複合操做啊,包括讀取inc的值,對其自增,而後再寫回主存。

假設線程A,讀取了inc的值爲10,這時候被阻塞了,由於沒有對變量進行修改,觸發不了volatile規則。

線程B此時也讀讀inc的值,主存裏inc的值依舊爲10,作自增,而後馬上就被寫回主存了,爲11。

此時又輪到線程A執行,因爲工做內存裏保存的是10,因此繼續作自增,再寫回主存,11又被寫了一遍。因此雖然兩個線程執行了兩次increase(),結果卻只加了一次。

有人說,volatile不是會使緩存行無效的嗎?可是這裏線程A讀取到線程B也進行操做以前,並無修改inc值,因此線程B讀取的時候,仍是讀的10。

又有人說,線程B將11寫回主存,不會把線程A的緩存行設爲無效嗎?可是線程A的讀取操做已經作過了啊,只有在作讀取操做時,發現本身緩存行無效,纔會去讀主存的值,因此這裏線程A只能繼續作自增了。

綜上所述,在這種複合操做的情景下,原子性的功能是維持不了了。可是volatile在上面那種設置flag值的例子裏,因爲對flag的讀/寫操做都是單步的,因此仍是能保證原子性的。

要想保證原子性,只能藉助於synchronized,Lock以及併發包下的atomic的原子操做類了,即對基本數據類型的 自增(加1操做),自減(減1操做)、以及加法操做(加一個數),減法操做(減一個數)進行了封裝,保證這些操做是原子性操做。

 

volatile底層原理

若是將使用volatile修飾的代碼和未使用volatile修飾的代碼都編譯成彙編語言,會發現,使用volatile修飾的代碼會多出一個lock前綴指令。

lock前綴指令至關於一個內存屏障,內存屏障的做用有如下三點:

①重排序時,不能把內存屏障後面的指令排序到內存屏障前

②使得本CPU的cache寫入內存

③寫入動做會引發其餘CPU緩存或內核的數據無效,至關於修改對其餘線程可見。

 

volatile的應用場景

由於volatile對複合操做無效,因此volatile修飾像上面例子中的flag這樣的只會發生讀/寫的標記型字段。

在單利模式中,volatile還能夠修飾成員變量,防止初始化時的指令重排序。

 1 class Singleton{
 2     private volatile static Singleton instance= null;
 3     
 4     private Singleton(){
 5         
 6     }
 7     
 8     public static Singleton getInstance(){
 9         if(instance==null){
10             synchronized(Singleton.class){
11                 if(instance==null){
12                     instance = new Singleton();
13                 }
14             }
15         }
16         return instance;
17     }
18 }
相關文章
相關標籤/搜索