Java內存模型之分析volatile
前篇博客【死磕Java並發】—–深入分析volatile的實現原理 中已經闡述了volatile的特性了:
- volatile可見性;對一個volatile的讀,總可以看到對這個變量最終的寫;
- volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是復合操作除外,例如i++;
- 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會限制重排序。其重排序規則如下:
翻譯如下:
- 如果第一個操作為volatile讀,則不管第二個操作是啥,都不能重排序。這個操作確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前;
- 當第二個操作為volatile寫是,則不管第一個操作是啥,都不能重排序。這個操作確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後;
- 當第一個操作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可以省略,其示意圖如下:
參考資料
- 方騰飛:《Java並發編程的藝術》
Java內存模型之分析volatile