Java併發—— 關鍵字volatile解析

午夜12點發表於2018-07-08

簡述

關鍵字volatile可以說是Java虛擬機器提供的最輕量級的同步機制,當一個變數定義為volatile,它具有記憶體可見性以及禁止指令重排序兩大特性,為了更好地瞭解volatile關鍵字,我們可以先看Java記憶體模型

Java記憶體模型

Java記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒擁有自己的工作記憶體,工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(讀寫)都必須在工作記憶體中進行,不同的執行緒之間無法直接訪問對方工作記憶體的變數。執行緒、主記憶體、工作記憶體關係:

Java併發—— 關鍵字volatile解析

以經典的i++為例,執行緒A從主記憶體獲取變數i值放入到工作記憶體的變數副本,然後在工作記憶體中將i+1,最後將新值同步到主記憶體中。從中我們可以看出簡單的i++,分了3個步驟,可以明顯發現線上程A從主記憶體獲取i值步驟後,可能有其他執行緒同步主記憶體中變數i的值,當執行緒A想要將i+1結果同步到主記憶體時就會出現不正確的結果,這是典型的執行緒不安全。

volatile特性

  • 可見性
  • 當一個執行緒修改了共享變數,其他執行緒能夠立即得知這個修改。Java記憶體模型通過在變數修改後將新值同步回主記憶體,volatile變數能保證新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理(synchronized和final兩個關鍵字也具備)。還是拿i++為例,volatile修飾的i可以確保,從主存中所獲取的變數i一定是最新的。

  • 有序性
  • 禁止指令重排序,程式執行的順序按照程式碼的先後順序執行。
    在執行程式時為了提高效能,編譯器和處理器常常會對指令做重排序。
    ①.編譯器重排序:編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序
    ②.處理器重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序

    從java原始碼到最終實際執行的指令序列,會分別經歷下面三種重排序:

    Java併發—— 關鍵字volatile解析

    1屬於編譯器重排序,2和3屬於處理器重排序。

    volatile使用場景

    在某些特定場景中,volatile相當於一個輕量級的sychronize,因為不會引起執行緒的上下文切換,但是使用volatile必須滿足兩個條件:
    ①.運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值
    ②.變數不需要與其他的狀態變數共同參與不變約束

    兩個使用場景:

  • 狀態標記
  • 使用volatile變數來控制併發,當shutdown()方法被呼叫時,能保證所有執行緒中執行的doWork()方法都立即停下來
    
      public class VolatileTest {
        private volatile boolean shutdownRequested;
    
       public void shutdown() {
           shutdownRequested = true;
       }
    
       public void doWork(){
           while (!shutdownRequested) {
            // 業務邏輯
          }    
      }
    }
    複製程式碼
    複製程式碼

    複製程式碼

  • DCL(雙鎖檢測)
  • 單例模式的一種實現方式
    
    
    public class Singleton {
    
        private volatile static Singleton singleton;
    
        public static Singleton getInstance() {
            if(singleton == null){
                synchronized (Singleton.class){
                    if(singleton == null){
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    複製程式碼
    複製程式碼

    複製程式碼

    volatile實現

    從硬體架構上來講,處理器使用寫緩衝區來臨時儲存向記憶體寫入的資料,可以減少對記憶體匯流排的佔用。雖然寫緩衝區有這麼多好處,但此操作僅對它所在的處理器可見,這個特性會對記憶體操作的執行順序產生重要的影響,由於操作緩衝區是非同步操作所以在外面看來,先寫後讀,還是先讀後寫,沒有嚴格的固定順序。

    volatile修飾的變數相對於普通變數會多出一個lock字首指令,這個操作相當於一個記憶體屏障(只有一個CPU訪問記憶體時,不需要記憶體屏障;但如果有兩個或更多CPU訪問同一塊記憶體,且其中有一個在觀測另一個,就需要記憶體屏障來保證一致性)。

    是否能重排序 第二個操作
    第一個操作 普通讀 普通寫 volatile讀 volatile寫
    普通讀 LoadStore
    普通寫 StoreStore
    volatile讀 LoadLoad LoadStore LoadLoad LoadStore
    volatile寫 StoreLoad StoreStore
    空白的單元格代表在不違反Java的基本語義下的重排是允許的。
    StoreStore屏障:保證在volatile寫之前,其前面的所有普通寫操作都已經重新整理到主記憶體
    StoreLoad屏障:避免volatile寫與後面可能有的volatile讀/寫操作重排序
    LoadLoad屏障:禁止處理器吧上面的volatile讀與下面的普通讀中排序
    LoadStore屏障:禁止處理器把上面的volatile讀與下面的普通寫重排序

    示例:
    
    public class VolatileTest {
        int a = 0;
        volatile int var1 = 1;
        volatile int var2 = 2;
    
    void readAndWrite() {
        int i = var1;   //volatile讀
        int j = var2;   //volatile讀
        a = i + i;      //普通讀
        var1 = i + 1;   //volatile寫
        var2 = j * 2;   //volatile寫
    }
    複製程式碼
    複製程式碼

    } 複製程式碼

    大致過程:

    Java併發—— 關鍵字volatile解析

    感謝

    1.《深入理解Java虛擬機器》
    2.佔小狼——面試必問的volatile,你瞭解多少?
    3.《Java併發程式設計的藝術》

    相關文章