併發程式設計之volatile與JMM多執行緒記憶體模型

字母哥部落格發表於2022-05-12

一、通過程式看現象

在開始為大家講解Java 多執行緒快取模型之前,我們先看下面的這一段程式碼。這段程式碼的邏輯很簡單:主執行緒啟動了兩個子執行緒,一個執行緒1、一個執行緒2。執行緒1先執行,sleep睡眠2秒鐘之後執行緒2執行。兩個執行緒使用到了一個共享變數shareFlag,初始值為false。如果shareFlag一直等於false,執行緒1將一直處於死迴圈狀態,所以我們線上程2中將shareFlag設定為true

public class VolatileTest {

  public static boolean shareFlag = false;

  public static void main(String[] args) throws InterruptedException {
    new Thread(() -> {
      System.out.print("開始執行執行緒1 =>");
      while (!shareFlag){  //shareFlag = false則一直死迴圈
        //System.out.println("shareFlag=" + shareFlag);
      }
      System.out.print("執行緒1執行完成 =>");
    }).start();

    Thread.sleep(2000);

    new Thread(() -> {
      System.out.print("開始執行執行緒2 =>");
      shareFlag = true;
      System.out.print("執行緒2執行完成 =>");
    }).start();
  }

}

如果你沒有學過JMM執行緒模型,可能你看完上面的程式碼,希望得到的輸出結果是下面這樣的:

開始執行執行緒1 =>開始執行執行緒2 =>執行緒2執行完成 =>執行緒1執行完成=>

如下圖所示,正常人理解這段程式碼,首先執行執行緒1進入迴圈,執行緒2修改shareFlag=true,執行緒1跳出迴圈。所以跳出迴圈的執行緒1會列印"執行緒1執行完成=>",但是經過筆者實驗,"執行緒1執行完成=>"不會被列印,執行緒1也沒有跳出死迴圈,這是為什麼呢?

二、為什麼會產生這種現象(JMM模型)?

要解釋上面提到的問題,我們就需要學習JMM(Java Memory Model)Java 記憶體模型,筆者覺得叫做Java多執行緒記憶體模型更準確一些。

  • 首先,在JMM中每個執行緒有自己的工作記憶體,在程式啟動的時候,執行緒將共享變數載入(read&load)到自己的工作記憶體中,載入到執行緒工作記憶體中的記憶體變數是主記憶體中共享變數的副本。也就是說此時shareFlag在記憶體中有三份,值都等於false。
  • 當執行緒2執行shareFlag=true的時候將其工作記憶體副本修改為shareFlag=true,同時將副本的值同步寫回(store&write)到主記憶體中。
  • 但是執行緒1的工作記憶體中的shareFlag=false沒有發生變化,所以執行緒1一直處於死迴圈之中

三、MESI 快取一致性協議

按照上文的實驗以及JMM模型,執行緒2修改的共享變數的值,執行緒1感知不到。那怎麼樣才能讓執行緒1感知到共享變數的值發生了變化呢?其實也很簡單,給shareFlag共享變數加上volatile關鍵字就可以了。

public volatile static boolean shareFlag = false;

其底層原理是這樣的,加上volatile關鍵字提示JMM遵循MESI 快取一致性協議,該協議包含如下的快取使用規範(看不懂可以不看,下文會用簡單的語言及例子描述一下)。

  1. Modified:代表當前Cache行的資料是修改過的(Dirty),並且只在當前CPU的Cache中是修改過的;此時該Cache行的資料與其他Cache中的資料不同,與記憶體中該行的資料也不同。
  2. Exclusive:代表當前Cache行的資料是有效資料,其他CPU的Cache中沒有這行資料;並且當前Cache行資料與記憶體中的資料相同。
  3. Shared:代表多個CPU的Cache中均快取有這行資料,並且Cache中的資料與記憶體中的資料一致;
  4. Invalid:表示當前Cache行中的資料無效;

上文中的快取使用規範可能過於複雜,簡單的說就是

  • 當執行緒2修改shareFlag的時候(參考Modify),告知bus匯流排我修改了共享變數shareFlag,
  • 執行緒1對Bus匯流排進行監聽,當它獲知共享變數shareFlag發生了修改就會將自己工作記憶體中的shareFlag副本刪除使其失效。
  • 當執行緒1再次需要使用到shareFlag的時候,發現工作記憶體中沒有shareFlag變數副本,就會重新從主記憶體中載入(read&load)

推薦閱讀《併發程式設計專欄》

歡迎關注我的部落格,更多精品知識合集

本文轉載註明出處(必須帶連線,不能只轉文字):字母哥部落格 - zimug.com

覺得對您有幫助的話,幫我點贊、分享!您的支援是我不竭的創作動力!。另外,筆者最近一段時間輸出瞭如下的精品內容,期待您的關注。

相關文章