首頁 >Java >java教程 >【死磕Java並發】-----Java記憶體模型之分析volatile

【死磕Java並發】-----Java記憶體模型之分析volatile

黄舟
黄舟原創
2017-02-24 10:07:471484瀏覽

前篇部落格【死磕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); 
        }
    }
}

【死磕Java並發】-----Java記憶體模型之分析volatile

上面透過一個例子稍微示範了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写
    }
}

沒有優化的範例圖如下:

【死磕Java並發】-----Java記憶體模型之分析volatile

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

1:這個肯定要保留了

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

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

4:保留

5:保留

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

7:保留

8:保留

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

【死磕Java並發】-----Java記憶體模型之分析volatile


 以上就是【死磕Java並發】-----Java記憶體模型之分析volatile的內容,更多相關內容請關注PHP中文網(www.php.cn)!


#
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn