1. 程式人生 > >Java內存模型之分析volatile

Java內存模型之分析volatile

b- oid HR 變量 before 深入 lba 避免 har

前篇博客【死磕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並發編程的藝術》

Java內存模型之分析volatile