Java的volatile關鍵字詳解

前言

在學習ConcurrentHashMap源碼的過程當中,發現本身對併發編程簡直是一無所知,所以打算從最基礎的volatile開始學習.html

volatile雖然很基礎,可是對於毫無JMM基礎的我來講,也是十分晦澀,看了許多文章仍然不能很好的表述出來.java

後來發現一篇文章(參考連接第一篇),給了我一些啓示:用回答問題的方式來學習知識及寫博客,由於對我這種新手來講,回答別人的問題,總比本身"演講"要來的容易許多.編程

volatile的用法

volatile只能夠用來修飾變量,不能夠修飾方法以及類緩存

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}
複製代碼

這是很經典的雙重鎖校驗實現的單例模式,想必不少人都看到過,代碼中可能會被多個線程訪問的singleton變量使用volatile修飾.安全

volatile的做用及原理

當一個變量被volatile修飾時,會擁有兩個特性:多線程

  1. 保證了不一樣線程對該變量操做的內存可見性.(當一個線程修改了變量,其餘使用次變量的線程能夠當即知道這一修改).
  2. 禁止了指令重排序.

1. 保證內存可見性

JMM操做變量的時候不是直接在主存進行操做的,而是每一個線程擁有本身的工做內存,在使用前,將該變量的值copy一份到本身的工做內存,讀取時直接讀取本身的工做內存中的值.寫入操做時,先將修改後的值寫入到本身的工做內存,再講工做內存中的值刷新回主存.併發

相似於下圖: ide

爲何這麼搞呢?固然是爲了提升效率,畢竟主存的讀寫相較於CPU中的指令執行都太慢了.post

這樣就會帶來一個問題.當執行性能

i = i + 1;(i初始化爲0)

語句時,單線程操做固然沒有問題,可是若是兩個線程操做呢?獲得的結果是2嗎?

不必定.

讓咱們詳細分解一下執行這句話的操做.

讀取內存中的i=0到工做內存(1)->工做內存中的i=i+1=1(2)- > 將工做內存中的i=1刷新回主存(3).

這是單線程操做的狀況,那麼假設當線程1執行到了(2)的時候,線程2開始了,進行完了(1)步驟,那麼這時候的狀況是什麼呢?

線程1位於(2),線程2位於(1).

線程1的工做內存中i=1,線程2的工做內存中i=0,以後分別進行餘下的步驟,最後拿到的結果爲1.

這是什麼緣由形成的呢?由於普通的變量沒有保證內存可見性.即:線程1已經修改了i的值,其餘的線程卻沒有獲得這個消息.

volatile保證了這一點,用volatile修飾的變量,讀取操做與普通變量相同.可是寫入操做發生後會當即將其刷新回主存,而且使其餘線程中對這一變量的緩存失效!

緩存失效了怎麼辦呢?去再次讀取主存唄,主存此時已經修改了(當即刷新了),則保證了內存可見性.

####小栗子:

public class VolatileTest {

  private static Boolean stop = false;//(1)
  private static volatile Boolean stop = false;//(2)


  public static void main(String args[]) throws InterruptedException {
    //新創建一個線程
    Thread testThread = new Thread() {
      @Override
      public void run() {
        System.out.println();
        int i = 1;
        //不斷的對i進行自增操做
        while (!stop) {
          i++;
        }
        System.out.println("Thread stop i=" + i);
      }
    };
    //啓動該線程
    testThread.start();
    //休眠一秒
    Thread.sleep(1000);
    //主線程中將stop置爲true
    stop = true;
    System.out.println(Thread.currentThread() + "now, in main thread stop is: " + stop);
    testThread.join();
  }

}
複製代碼

這段代碼在主線程的第二行定義了一個布爾變量stop, 而後主線程啓動一個新線程,在線程裏不停得增長計數器i的值,直到主線程的布爾變量stop被主線程置爲true才結束循環。

主線程用Thread.sleep停頓1秒後將布爾值stop置爲true。

所以,咱們指望的結果是,上述Java代碼執行1秒鐘後中止,而且打印出1秒鐘內計數器i的實際值。

然而,執行這個Java應用後,你發現它進入了死循環,程序沒有中止.

(1)處的代碼改成(2)處的,即對stop的變量添加volatile修飾,你會發現程序如咱們預期的那樣中止了.

2.禁止指令重排序

JVM在不影響單線程執行結果的狀況下回對指令進行重排序,好比:

int i = 1;//(1)
int j = 2;//(2)
int h = i * j;//(3)
複製代碼

上述代碼中,(3)執行依賴於(1)(2)的執行,可是(1)(2)的執行順序並不影響結果,也就是說當咱們進行了上述的編碼,JVM真正執行的多是(1)(2)(3),也多是(2)(1)(3).

這在單線程中是無所謂的,還會帶來性能的提高.

可是在多線程中就會出現問題,好比下面的代碼:

//線程1
context = loadContext();//(1)
inited = true;//(2)


//線程2
while(!inited ){ //根據線程A中對inited變量的修改決定是否使用context變量
   sleep(100);
}
doSomethingwithconfig(context);
複製代碼

若是每一個線程中的指令都順序執行,則沒有問題,可是在線程1中,兩個語句並沒有依賴關係,所以可能會發生重排序,若是發生了重排序:

inited = true;//(2)
context = loadContext();//(1)
複製代碼

線程1重排序以後先執行了(2)語句,在線程2中,程序跳出了循環,執行doSomethingwithconfig,由於他認爲context已經進行了初始化,而後並無,就會出現錯誤.

使用volatile關鍵字修飾inited變量,JVM就會阻止對inited相關的代碼進行重排序.這樣就可以按照既定的順序指執行.

volatile總結

volatile是輕量級同步機制,與synchronized相比,他的開銷更小一些,同時安全性也有所下降,在一些特定的場景下使用它能夠在完成併發目標的基礎上有一些性能上的優點.可是同時也會帶來一些安全上的問題,且比較難以排查,使用時須要謹慎.

volatile的使用場景

使用volatile修飾的變量最好知足如下條件:

  1. 對變量的寫操做不依賴於當前值
  2. 該變量沒有包含在具備其餘變量的不變式中

這裏舉幾個比較經典的場景:

  1. 狀態標記量,就是前面例子中的使用.
  2. 一次性安全發佈.雙重檢查鎖定問題(單例模式的雙重檢查).
  3. 獨立觀察.若是系統須要使用最後登陸的人員的名字,這個場景就很適合.
  4. 開銷較低的「讀-寫鎖」策略.當讀操做遠遠大於寫操做,能夠結合使用鎖和volatile來提高性能.

注意事項

volatile並不能保證操做的原子性,想要保證原子性請使用synchronized關鍵字加鎖.

參考連接

www.techug.com/post/java-v… www.importnew.com/23535.html

完。





ChangeLog

2018-11-22 完成

以上皆爲我的所思所得,若有錯誤歡迎評論區指正。

歡迎轉載,煩請署名並保留原文連接。

聯繫郵箱:huyanshi2580@gmail.com

更多學習筆記見我的博客------>呼延十

相關文章
相關標籤/搜索