volatile底層原理詳解

JavaStorm發表於2019-05-29

今天我們聊聊volatile底層原理;

Java語言規範對於volatile定義如下:

Java程式語言允許執行緒訪問共享變數,為了確保共享變數能夠被準確和一致性地更新,執行緒應該確保通過排它鎖單獨獲得這個變數。

首先我們從定義開始入手,官方定義比較拗口。通俗來說就是一個欄位被volatile修飾,Java的記憶體模型確保所有的執行緒看到的這個變數值是一致的,但是它並不能保證多執行緒的原子操作。這就是所謂的執行緒可見性。我們要知道他是不能保證原子性的

記憶體模型相關概念

Java執行緒之間的通訊由Java記憶體模型(JMM)控制,JMM決定一個執行緒對共享變數的修改何時對另外一個執行緒可見。JMM定義了執行緒與主記憶體的抽象關係:執行緒之間的變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory)儲存著共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。

image

如果執行緒A與執行緒B通訊:

  1. 執行緒A要先把本地記憶體A中更新過的共享變數刷寫到主記憶體中。

  2. 執行緒B到主記憶體中讀取執行緒A更新後的共享變數

計算機在執行程式時,每條指令都是在CPU中執行的,在執行過程中勢必會涉及到資料的讀寫。我們知道程式執行的資料是儲存在主存中,這時就會有一個問題,讀寫主存中的資料沒有CPU中執行指令的速度快,如果任何的互動都需要與主存打交道則會大大影響效率,所以就有了CPU快取記憶體。CPU快取記憶體為某個CPU獨有,只與在該CPU執行的執行緒有關。

有了CPU快取記憶體雖然解決了效率問題,但是它會帶來一個新的問題:資料一致性。在程式執行中,會將執行所需要的資料複製一份到CPU快取記憶體中,在進行運算時CPU不再也主存打交道,而是直接從快取記憶體中讀寫資料,只有當執行結束後才會將資料重新整理到主存中。

舉個例子:

i++;

當執行緒執行這行程式碼時,首先會從主記憶體中讀取i,然後複製一份到CPU快取記憶體中,接著CPU執行+1的操作,再將+1後的資料寫在快取中,最後一步才是重新整理到主記憶體中。在單執行緒時沒有問題,多執行緒就有問題了。

如下:假如有兩個執行緒A、B都執行這個操作(i++),按照我們正常的邏輯思維主存中的i值應該=3,但事實是這樣麼?

分析如下:

兩個執行緒從主存中讀取i的值(1)到各自的快取記憶體中,然後執行緒A執行+1操作並將結果寫入快取記憶體中,最後寫入主存中,此時主存i==2,執行緒B做同樣的操作,主存中的i仍然=2。所以最終結果為2並不是3。這種現象就是快取一致性問題。

解決快取一致性方案有兩種:

  1. 通過在匯流排加LOCK#鎖的方式;

  2. 通過快取一致性協議。

但是方案1存在一個問題,它是採用一種獨佔的方式來實現的,即匯流排加LOCK#鎖的話,只能有一個CPU能夠執行,其他CPU都得阻塞,效率較為低下。

第二種方案,快取一致性協議(MESI協議)它確保每個快取中使用的共享變數的副本是一致的。所以JMM就解決這個問題。

volatile實現原理

有volatile修飾的共享變數進行寫操作的時候會多出Lock字首的指令,該指令在多核處理器下會引發兩件事情。

  1. 將當前處理器快取行資料刷寫到系統主記憶體。

  2. 這個刷寫回主記憶體的操作會使其他CPU快取的該共享變數記憶體地址的資料無效。

這樣就保證了多個處理器的快取是一致的,對應的處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器快取行設定無效狀態,當處理器對這個資料進行修改操作的時候會重新從主記憶體中把資料讀取到快取裡。

使用場景

volatile經常用於兩個場景:狀態標記、double check

  1. 狀態標記
//執行緒1
boolean stop = false;
while(!stop){
   doSomething();
}

//執行緒2
stop = true;

這段程式碼是很典型的一段程式碼,很多人在中斷執行緒時可能都會採用這種標記辦法。但是事實上,這段程式碼會完全執行正確麼?即一定會將執行緒中斷麼?不一定,也許在大多數時候,這個程式碼能夠把執行緒中斷,但是也有可能會導致無法中斷執行緒(雖然這個可能性很小,但是隻要一旦發生這種情況就會造成死迴圈了)。

下面解釋一下這段程式碼為何有可能導致無法中斷執行緒。在前面已經解釋過,每個執行緒在執行過程中都有自己的工作記憶體,那麼執行緒1在執行的時候,會將stop變數的值拷貝一份放在自己的工作記憶體當中。

那麼當執行緒2更改了stop變數的值之後,但是還沒來得及寫入主存當中,執行緒2轉去做其他事情了,那麼執行緒1由於不知道執行緒2對stop變數的更改,因此還會一直迴圈下去。

但是加上volatile就沒問題了。如下所示:

    volatile boolean flag = false;

    while(!flag){
       doSomething();
    }

    public void setFlag() {
       flag = true;
    }

    volatile boolean inited = false;
    //執行緒1:
    context = loadContext();  
    inited = true;            

    //執行緒2:
    while(!inited ){
    sleep()
    }
    doSomethingwithconfig(context);
  1. double check
public class Singleton{
   private volatile static Singleton instance = null;

   private Singleton() {

  }

   public static Singleton getInstance() {
       if(instance==null) {
           synchronized (Singleton.class) {
               if(instance==null)
                   instance = new Singleton();
          }
      }
       return instance;
  }
}

客官覺得有用請點贊或收藏,關注公眾號JavaStorm,你將發現一個有趣的靈魂!
後面我們繼續分析JMM記憶體模型相關技術。
將自己的知識分享,以後會持續輸出,希望給讀者朋友們帶來幫助。若有幫助讀者朋友可以點贊或者關注。
JavaStorm.png

相關文章