死磕Java併發:Java記憶體模型之分析volatile
本文轉載自公眾號: Java技術驛站
volatile可見性;對一個volatile的讀,總可以看到對這個變數最終的寫;
volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是複合操作除外,例如i++;
JVM底層採用“記憶體屏障”來實現volatile語義。
下面LZ就通過happens-before原則和volatile的記憶體語義兩個方向介紹volatile。
1、volatile與happens-before
在這篇《死磕Java併發:深入分析volatile的實現原理》文章中,LZ闡述了happens-before是用來判斷是否存資料競爭、執行緒是否安全的主要依據,它保證了多執行緒環境下的可見性。
下面我們就那個經典的例子來分析volatile變數的讀寫建立的happens-before關係。
publicclassVolatileTest{int i =0;volatileboolean flag =false;//Thread Apublicvoid write(){ i =2;//1 flag =true;//2}//Thread Bpublicvoid read(){if(flag){//3System.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可見。
2、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例子分析下:
publicclassVolatileTest{int i =0;volatileboolean flag =false;publicvoid write(){ i =2; flag =true;}publicvoid read(){if(flag){System.out.println("---i = "+ i);}}}
上面通過一個例子稍微演示了volatile指令的記憶體屏障圖例。
volatile的記憶體屏障插入策略非常保守,其實在實際中,只要不改變volatile寫-讀得記憶體語義,編譯器可以根據具體情況優化,省略不必要的屏障。如下(摘自方騰飛 《Java併發程式設計的藝術》):
publicclassVolatileBarrierExample{int a =0;volatileint v1 =1;volatileint 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併發程式設計的藝術》
- END -
往期推薦:
死磕Java系列:
……
Spring系列:
……
號外!
最近在做幾個有意思的開源專案,感興趣的朋友可以看看。
地址:
https://github.com/dyc87112/swagger-butler
可關注我的公眾號
深入交流、更多福利
掃碼加入我的知識星球
點選“閱讀原文”,看本號其他精彩內容