1. 程式人生 > 實用技巧 >JMM記憶體模型和 volatile快取一致性

JMM記憶體模型和 volatile快取一致性

JMM記憶體模型和 volatile快取一致性

一、 JMM記憶體模型

Java執行緒記憶體模型與CPU快取模型型別,是基於CPU快取模型來建立的,Java執行緒記憶體模型是標準化的,遮蔽了底層不同計算機的區別。

CPU快取模型

JMM記憶體模型

當多個執行緒共享一個變數時,會從主記憶體拷貝一份副本到自己執行緒的工作記憶體中。

假如執行緒A和執行緒B進行通訊

  1. 執行緒A從主記憶體中將共享變數讀入執行緒A的工作記憶體後並進行操作,之後將資料重新寫回到主記憶體中;

  2. 執行緒B從主存中讀取最新的共享變數

從橫向去看看,執行緒A和執行緒B就好像通過共享變數在進行隱式通訊。這其中有很有意思的問題,如果執行緒A更新後資料並沒有及時寫回到主存,而此時執行緒B讀到的是過期的資料,這就出現了“髒讀”現象。可以通過同步機制(控制不同執行緒間操作發生的相對順序)來解決或者通過volatile關鍵字使得每次volatile變數都能夠強制重新整理到主存,從而對每個執行緒都是可見的。

事實上,不同執行緒無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體。

二、 volatile關鍵字

synchronized是阻塞式同步,線上程競爭激烈的情況下會升級為重量級鎖。

而volatile就可以說是java虛擬機器提供的最輕量級的同步機制.

volatile語義有如下兩個作用:

  • 可見性:保證被volatile修飾的共享變數對所有執行緒總數可見的,也就是當一個執行緒修改了被volatile修飾的共享變數的值,新值總是可以被其他執行緒立即得知。

  • 有序性:禁止指令重排序優化。

volatile 快取可見性實現原理

  • JMM記憶體互動層面:volatile修飾的變數的read、load、use操作和assign、store、write必須是連續的,即修改後必須立即同步回主記憶體,使用時必須從主記憶體重新整理,由此保證volatile的 可見性

  • 底層實現:通過 彙編lock字首指令 ,它會鎖定 變數快取區域 並寫回主記憶體,這個操作稱為“快取鎖定”,快取一致性機制會阻止同時修改兩個以上處理器快取的記憶體區域資料。一個處理器的快取回寫到記憶體會導致其他處理器的快取 失效

彙編程式碼檢視volatile底層實現

-XX:+UnlockDianosticVMOptions -XX:+PrintAssembly -Xcomp

通過程式碼來分析加了volatile關鍵字底層發生了什麼

public class VolatileTest {
  private static boolean initFlag = false;
​
  public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("waiting data...");
        while (!initFlag){
​
         }
        System.out.println("=============success");
       }
     }).start();
    Thread.sleep(2000);
​
    new Thread(new Runnable() {
      @Override
      public void run() {
        prepareData();
       }
     }).start();
   }
  public static void prepareData(){
    System.out.println("preparing data...");
    initFlag = true;
    System.out.println("prepare data end...");
   }
}
​

輸出結果:

waiting data...
preparing data...
prepare data end...

通過執行程式和分析結果,可以看出,一直卡在while迴圈處,也就是 initFlag 在第二個執行緒 prepareData() 中被置為 true並不能改變第一個執行緒中initFlag的值。所以,才一直卡在while處,也沒有輸出 “ success”。

修改程式碼

用volatile修飾initFlag。

private static volatile boolean initFlag = false;

輸出結果

waiting data...
preparing data...
prepare data end...
=============success

可以發現,第一個執行緒的initFlag能被置為true了,才能輸出“success”。這是為什麼呢?

分析

1. 先來看 JMM 中資料的8個原子操作

JMM資料原子操作:

  • lock(鎖定): 作用於主記憶體, 將主記憶體變數 加鎖,標識為執行緒獨佔狀態

  • read(讀取): 作用於主記憶體,從主記憶體讀取變數值傳送到執行緒工作記憶體中

  • load(載入): 作用於工作記憶體, 將read到的資料放入工作記憶體中的 變數副本

  • use(使用): 作用於工作記憶體,將工作記憶體中的值傳遞給 執行引擎(計算),每當虛擬機器遇到一個需要使用這個變數的指令時,將會執行這個動作

  • assign(賦值): 作用於工作記憶體,將計算好的值重新賦值到工作記憶體中,每當虛擬機器遇到一個需要使用這個變數的指令時,將會執行這個動作

  • store(儲存): 作用於工作記憶體 ,將工作記憶體資料傳送給主記憶體中,以備隨後的write操作使用

  • write(寫入): 作用於主記憶體, 將store傳遞值賦值給主記憶體中的變數

  • unlock(解鎖): 作用於主記憶體 ,將主記憶體變數解鎖,解鎖後其他執行緒可以鎖定該變數,被其他執行緒鎖定。

2. MESI快取一致性協議

多個CPU從主記憶體讀取同一個資料到各自的快取記憶體,當其中某個cpu修改了快取裡的資料,該資料會 馬上同步回主記憶體(即使方法還沒執行完) ,其他cpu通過 匯流排嗅探機制 可以感知到資料的變化從而 將自己快取裡的資料失效

3. 結合8個原子操作和快取一致性分析程式碼的 volatile

畫圖分析(volatile會開啟快取一致性協議)

分析

  1. 首先,主記憶體中,initFlag=false。

  2. 執行緒1經過 read、load,工作記憶體處,initFlag = false,use 之後, 執行引擎處 !initFlag = true,此時,卡在while處。

  3. 執行緒2同理,經過 read、load 之後,此時工作記憶體處,initFlag=false,經過 use (initFlag=true)、assign後,initFlag=true。

  4. 因為執行緒2處的cpu修改了 initFlag 的值,會 馬上回寫 到主記憶體中(經過 storewrite兩步)。

  5. 執行緒1處的cpu 通過 匯流排嗅探機制 嗅探到變化,會將工作記憶體中的資料 失效(initFlag=false失效)

  6. 執行緒1會 重新 去主記憶體 read 最新的資料(此時,主記憶體中的資料 initFlag=true)。

  7. 那麼,執行緒1在讀取最新的資料時,執行引擎處,!initFlag = false,結束迴圈,輸出 “success”。

而在早期是通過 匯流排加鎖 的方式去解決的。

cpu從主記憶體讀取資料到快取記憶體,會在匯流排對這個資料加鎖,這樣其他cpu沒法去讀或寫這個資料,知道這個cpu使用完資料釋放鎖之後其它cpu才能讀取該資料。

4. 8個原子操作有的約束

  1. 不允許read和load、store和write操作之一單獨出現(即不允許一個變數從主存讀取了但是工作記憶體不接受,或者從工作記憶體發起會寫了但是主存不接受的情況),以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的。

  2. 不允許一個執行緒丟棄它的最近的assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。

  3. 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從執行緒的工作記憶體同步回主記憶體中。

  4. 一個新的變數只能從主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數,換句話說就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。

  5. 一個變數在同一個時刻只允許一條執行緒對其執行lock操作,但lock操作可以被同一個條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。

  6. 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。

  7. 如果一個變數實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數。

  8. 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體(執行store和write操作)。

參考

Java記憶體模型以及happens-before.md

java併發記憶體模型以及記憶體操作規則(八種原子操作)

《深入理解java虛擬機器》第二版