歡迎光臨
每天分享高質量文章

【死磕Java併發】—–Java記憶體模型之分析volatile

前篇部落格【死磕Java併發】—–深入分析volatile的實現原理 中已經闡述了volatile的特性了:

  1. volatile可見性;對一個volatile的讀,總可以看到對這個變數最終的寫;

  2. volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是複合操作除外,例如i++;

  3. JVM底層採用“記憶體屏障”來實現volatile語意

下麵LZ就透過happens-before原則和volatile的記憶體語意兩個方向介紹volatile。

volatile與happens-before

在這篇部落格【死磕Java併發】—–Java記憶體模型之happend-before中LZ闡述了happens-before是用來判斷是否存資料競爭、執行緒是否安全的主要依據,它保證了多執行緒環境下的可見性。下麵我們就那個經典的例子來分析volatile變數的讀寫建立的happens-before關係。

public class VolatileTest {

    int i = 0;    
   volatile boolean flag = false;    //Thread A    public void write(){        i = 2;              //1        flag = true;        //2    }    
   
   //Thread B    public void read(){        
       if(flag){                                   //3            System.out.println("---i = " + i);      //4        }    } }

依據happens-before原則,就上面程式得到如下關係:

  • 依據happens-before程式順序原則:1 happens-before 2、3 happens-before 4;

  • 根據happens-before的volatile原則:2 happens-before 3;

  • 根據happens-before的傳遞性:1 happens-before 4

操作1、操作4存在happens-before關係,那麼1一定是對4可見的。可能有同學就會問,操作1、操作2可能會發生重排序啊,會嗎?如果看過LZ的部落格就會明白,volatile除了保證可見性外,還有就是禁止重排序。所以A執行緒在寫volatile變數之前所有可見的共享變數,在執行緒B讀同一個volatile變數後,將立即變得對執行緒B可見。

volataile的記憶體語意及其實現

在JMM中,執行緒之間的通訊採用共享記憶體來實現的。volatile的記憶體語意是:

當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值立即掃清到主記憶體中。 
當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體設定為無效,直接從主記憶體中讀取共享變數

所以volatile的寫記憶體語意是直接掃清到主記憶體中,讀的記憶體語意是直接從主記憶體中讀取。 
那麼volatile的記憶體語意是如何實現的呢?對於一般的變數則會被重排序,而對於volatile則不能,這樣會影響其記憶體語意,所以為了實現volatile的記憶體語意JMM會限制重排序。其重排序規則如下:

翻譯如下:

  1. 如果第一個操作為volatile讀,則不管第二個操作是啥,都不能重排序。這個操作確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前;

  2. 當第二個操作為volatile寫是,則不管第一個操作是啥,都不能重排序。這個操作確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後;

  3. 當第一個操作volatile寫,第二操作為volatile讀時,不能重排序。

volatile的底層實現是透過插入記憶體屏障,但是對於編譯器來說,發現一個最優佈置來最小化插入記憶體屏障的總數幾乎是不可能的,所以,JMM採用了保守策略。如下:

  • 在每一個volatile寫操作前面插入一個StoreStore屏障

  • 在每一個volatile寫操作後面插入一個StoreLoad屏障

  • 在每一個volatile讀操作後面插入一個LoadLoad屏障

  • 在每一個volatile讀操作後面插入一個LoadStore屏障

StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作都已經掃清到主記憶體中。

StoreLoad屏障的作用是避免volatile寫與後面可能有的volatile讀/寫操作重排序。

LoadLoad屏障用來禁止處理器把上面的volatile讀與下麵的普通讀重排序。

LoadStore屏障用來禁止處理器把上面的volatile讀與下麵的普通寫重排序。

下麵我們就上面那個VolatileTest例子分析下:

public class VolatileTest {
        int i = 0;    
       volatile boolean flag = false;
          
       public void write(){            i = 2;            flag = true;        }    
       public void read(){        
           if(flag){                System.out.println("---i = " + i);        }    } }

上面透過一個例子稍微演示了volatile指令的記憶體屏障圖例。

volatile的記憶體屏障插入策略非常保守,其實在實際中,只要不改變volatile寫-讀得記憶體語意,編譯器可以根據具體情況最佳化,省略不必要的屏障。如下(摘自方騰飛 《Java併發程式設計的藝術》):

public class VolatileBarrierExample {
    int a = 0;    volatile int v1 = 1;    volatile int v2 = 2;    void readAndWrite(){        int i = v1;     //volatile讀
        int j = v2;     //volatile讀
        a = i + j;      //普通讀
        v1 = i + 1;     //volatile寫
        v2 = j * 2;     //volatile寫
    }
}

沒有最佳化的示例圖如下:

我們來分析上圖有哪些記憶體屏障指令是多餘的

1:這個肯定要保留了

2:禁止下麵所有的普通寫與上面的volatile讀重排序,但是由於存在第二個volatile讀,那個普通的讀根本無法越過第二個volatile讀。所以可以省略。

3:下麵已經不存在普通讀了,可以省略。

4:保留

5:保留

6:下麵跟著一個volatile寫,所以可以省略

7:保留

8:保留

所以2、3、6可以省略,其示意圖如下:

參考資料

  1. 方騰飛:《Java併發程式設計的藝術》

END

贊(0)

分享創造快樂