快速理解 volatile 關鍵字

前言

  看了不少 Java 併發編程書籍的目錄,volatile 在 JMM 中老是單獨拎出來做爲一個章節來說,主要是由於它的特殊規則。要完全弄懂 volatile 不太容易,可是若是從它如何解決併發編程中的可見性、原子性和有序性問題來學習,就能很快掌握 volatile 的做用。學習 volatile 關鍵字頗有必要,Java 併發工具中的不少類都是基於 volatile 的。java

volatile 特性

  在 JMM 中 volatile 的三大特性以下:數據庫

  1. 保證可見性:當寫一個 volatile 變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存,使其餘線程當即可見。
  2. 保證有序性:當變量被修飾爲 volatile 時,JMM 會禁止讀寫該變量先後語句的大部分重排序優化,以保證變量賦值操做的順序與程序中的執行順序一致。
  3. 部分原子性:對任意單個 volatile 變量的讀/寫具備原子性,但相似於 volatile++ 這種複合操做不具備原子性。

如何保證可見性

  volatile 變量可見性不少書上都喜歡放到 happens-before 原則中來說:編程

對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
複製代碼

  其實我以爲這句話初看並不能很好的理解 volatile 的可見性,並且還會引入新的概念 happens-before 規則。換一種表述方式會容易理解的多,其在 JMM 中的寫和讀語義以下:多線程

  1. 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
  2. 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

  這就保證了 volatile 變量的可見性,也解釋了 happens-before 中的 volatile 規則,並且須要注意的是:在寫和讀時操做的是整個工做內存中的共享變量,因此在讀 volatile 變量時工做內存中的其餘共享變量也是最新的。併發

如何保證有序性

  volatile 的有序性可能比較晦澀,可是看完 JMM 針對編譯器制定的 volatile 重排序規則表後就會很容易理解:app

  由上圖 1 可知,JMM 限制了大部分狀況下 volatile 變量讀寫語句先後語句的重排序,結合圖片來看看下個這個例子:

class OrderingExample {
    int x = 0;
    volatile boolean flag = false;
    public void writer() {
        x = 42; //宇宙的終極答案
        flag = true;
    }
    public void reader() {
        if (flag == true) {
            //x = ?
        }
    }
}
複製代碼

  以上代碼在並發編程前傳 中講有序性的時候也貼過,這裏將 flag 定義成 volatile。若是線程 A 先執行完 writer(),線程 B 後執行到 reader() 中的 x= 的時候,x 必定等於 42(JDK 1.5 之後),緣由以下:工具

  參考圖 1,能夠看出普通變量的寫不能重排到 volatile 變量的寫後面,因此便不存在有序性問題。 其餘禁止重排序規則參考圖 1 進行類推,整個規則讓 JMM 在多線程環境下保證了 volatile 變量的有序性。在本規則中有如下兩點須要注意:性能

  1. 只要 volatile 變量與普通變量之間的重排序可能會破壞 volatile 的內存語義,這種重排序就會被編譯器重排序規則和處理器內存屏障插入策略禁止。換句話說,若是沒有破壞 volatile 的內存語義則能夠重排序,參考圖 1 空白格子對應的規則。學習

  2. 爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序,細則以下:優化

    在每一個volatile寫操做的前面插入一StoreStore屏障。 
     在每一個volatile寫操做的後面插入一個StoreLoad屏障。
     在每一個volatile讀操做的後面插入一個LoadLoad屏障。
     在每一個volatile讀操做的後面插入一個LoadStore屏障。
    複製代碼

如何保證部分原子性

  一樣拿併發編程前傳中 dobule 和 long 的例子,double 和 long 變量的單個讀/寫在絕大部分商業虛擬機上都是原子的,但在在極端狀況下並不具備原子性,而加了 volatile 後就必定能保證單個讀/寫原子性。這由 JMM 保證,其中底層原理有待深究,但底層應該是經過 cpu 指令來實現的。

  之因此說只能保證部分原子性,是由於 volatile 並不能保證 volatile 變量參與的複合語句的原子性,好比 i++; i+=1; 等這種看上去是單讀和寫,實質須要先讀後寫的語句。

與 synchronized 的區別

  因爲 volatile 僅僅保證對單個 volatile 變量的讀/寫具備原子性,而鎖的互斥執行的特性能夠確保對整個臨界區代碼的執行具備原子性。在功能上,鎖比 volatile 更強大;在可伸縮性和執行性能上,volatile 更有優點。若是讀者想在程序中用volatile代替鎖,請必定謹慎。即便是單個變量的語句,也只有如下三種狀況下可使用 volatile 代替鎖:

  1. 對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
  2. 該變量不會與其餘狀態變量一塊兒歸入不變性條件中。
  3. 在訪問變量時不須要加鎖。

  對於 1 的前半句是指對變量的寫以前不能還要去讀它,好比相似 i++、i = i + 1 等語句。至於 1 的後半句相似於咱們常見的一寫多讀模型,不存在多線程問題。

  對於 2 是指該變量不能與其餘變量一塊兒控制某個操做,好比 if( i < j ){},其中 i 和 j 都是共享變量,i 是 volatile 修飾的。又好比 while( i - j > 2){} 等。i 與其餘共享變量 j 一塊兒參與了不變的條件控制,故存在問題。

  在《Java 併發編程實戰》中列出了第 3 點,而《深刻理解 Java 虛擬機》中直接刪去了。可見對於 3 是不言而喻的。

總結

  瞭解 volatile 的三大特性之後,回看阿里數據庫大牛何登成關於 volatile 的文章《C/C++ volatile關鍵詞深度剖析》理解起來不要太簡單。理解 volatile 簡單,若是想靈活應用 volatile 能夠看看 Java 併發工具包中的一些源碼實現,看看大牛如何把 volatile 運用的恰到好處的。

相關文章
相關標籤/搜索