每日一問:談談 volatile 關鍵字

南塵發表於2019-06-20

這是 wanAndroid 每日一問中的一道題,下面我們來嘗試解答一下。

講講併發專題 volatile,synchronize,CAS,happens before, lost wake up

為了本系列的「短平快」,今天我們就來第一個主角:volatile

保證記憶體可見性

前面我們講到:Java 記憶體模型分為了主記憶體和工作記憶體兩部分,其規定程式所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作(賦值、讀取等)都必須在工作記憶體中進行,而不能直接讀取主記憶體中的變數。不同執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞都必須經過主記憶體的傳遞來完成。
每日一問:談談 volatile 關鍵字

這樣就會存在一個情況,工作記憶體值改變後到主記憶體更新一定是需要一定時間的,所以可能會出現多個執行緒操作同一個變數的時候出現取到的值還是未更新前的值。

這樣的情況我們通常稱之為「可見性」,而我們加上 volatile 關鍵字修飾的變數就可以保證對所有執行緒的可見性。

這裡的可見性是什麼意思呢?當一個執行緒修改了變數的值,新的值會立刻同步到主記憶體當中。而其他執行緒讀取這個變數的時候,也會從主記憶體中拉取最新的變數值。

為什麼 volatile 關鍵字可以有這樣的特性?這得益於 Java 語言的先行發生原則(happens-before)。簡單地說,就是先執行的事件就應該先得到結果。

但是! volatile 並不能保證併發下的安全。

Java 裡面的運算並非原子操作,比如 i++ 這樣的程式碼,實際上,它包含了 3 個獨立的操作:讀取 i 的值,將值加 1,然後將計算結果返回給 i。這是一個「讀取-修改-寫入」的操作序列,並且其結果狀態依賴於之前的狀態,所以在多執行緒環境下存在問題。

要解決自增操作在多執行緒下執行緒不安全的問題,可以選擇使用 Java 提供的原子類,如 AtomicInteger 或者使用 synchronized 同步方法。

原子性:在 Java 中,對基本資料型別的變數的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變數)才是原子操作。(變數之間的相互賦值不是原子操作,比如 y = x,實際上是先讀取 x 的值,再把讀取到的值賦值給 y 寫入工作記憶體)

禁止指令重排

最開始看到「指令重排」這個詞語的時候,我也是一臉懵逼。後面看了相關書籍才知道,處理器為了提高程式效率,可能對輸入程式碼進行優化,它不保證各個語句的執行順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。

指令重排是一把雙刃劍,雖然優化了程式的執行效率,但是在某些情況下,卻會影響到多執行緒的執行結果。比如下面的程式碼:

boolean contextReady = false;
//線上程A中執行:
context = loadContext();    // 步驟 1
contextReady = true;        // 步驟 2

//線上程B中執行:
while(!contextReady ){ 
   sleep(200);
}
doAfterContextReady (context);

以上程式看似沒有問題。執行緒 B 迴圈等待上下文 context 的載入,一旦 context 載入完成,contextReady == true 的時候,才執行 doAfterContextReady 方法。
但是,如果執行緒 A 執行的程式碼發生了指令重排,也就是上面的步驟 1 和步驟 2 調換了順序,那執行緒 B 就會直接跳出迴圈,直接執行 doAfterContextReady() 方法導致出錯。

volatile 採用「記憶體屏障」這樣的 CPU 指令就解決這個問題,不讓它指令重排。

使用場景

從上面的總結來看,我們非常容易得出 volatile 的使用場景:

  1. 執行結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值。
  2. 變數不需要與其他的狀態變數共同參與不變約束。

比如下面的場景,就很適合使用 volatile 來控制併發,當 shutdown() 方法呼叫的時候,就能保證所有執行緒中執行的 work() 立即停下來。

volatile boolean shutdownRequest;
private void shutdown(){
    shutdownRequest = true;
}
private void work(){
    while (!shutdownRequest){
        // do something
    }
}

總結

說了這麼多,其實對於 volatile 我們只需要知道,它主要特性:保證可見性、禁止指令重排、解決 long 和 double 的 8 位元組賦值問題。

還有一個比較重要的是:它並不能保證併發安全,不要和 synchronize 混淆。

細心的你還會發現,在 Kotlin 語言中,其實是沒有 volatilesynchronize 這樣的關鍵字的,那 Kotlin 是怎麼處理併發問題的呢?感興趣的一定要去看看。

文章參考:
漫畫:什麼是volatile關鍵字?(整合版)
《深入理解 Java 虛擬機器》

相關文章