摘要:為了實現的內存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。上述寫和讀的內存屏障插入策略非常保守。
本講座地址https://segmentfault.com/l/15... 歡迎大家圍觀
Java的Volatile的特征是任何讀都能讀到最新值,本質上是JVM通過內存屏障來實現的,讓我們看看從字節(jié)碼以及匯編碼的角度,來看下是否真是如此?
一 Volatile與內存屏障本節(jié)內容來自:http://www.infoq.com/cn/artic...
為了實現volatile內存語義,JMM會分別限制重排序類型。下面是JMM針對編譯器制定的volatile重排序規(guī)則表:
舉例來說,第三行最后一個單元格的意思是:在程序順序中,當第一個操作為普通變量的讀或寫時,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。
從上表我們可以看出:
當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規(guī)則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規(guī)則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
為了實現volatile的內存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現一個最優(yōu)布置來最小化插入屏障的總數幾乎不可能,為此,JMM采取保守策略。下面是基于保守策略的JMM內存屏障插入策略:
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的后面插入一個StoreLoad屏障。
在每個volatile讀操作的后面插入一個LoadLoad屏障。
在每個volatile讀操作的后面插入一個LoadStore屏障。
上述內存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內存語義。
下面是保守策略下,volatile寫插入內存屏障后生成的指令序列示意圖:
上圖中的StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經對任意處理器可見了。這是因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內存。
這里比較有意思的是volatile寫后面的StoreLoad屏障。這個屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因為編譯器常常無法準確判斷在一個volatile寫的后面,是否需要插入一個StoreLoad屏障(比如,一個volatile寫之后方法立即return)。為了保證能正確實現volatile的內存語義,JMM在這里采取了保守策略:在每個volatile寫的后面或在每個volatile讀的前面插入一個StoreLoad屏障。從整體執(zhí)行效率的角度考慮,JMM選擇了在每個volatile寫的后面插入一個StoreLoad屏障。因為volatile寫-讀內存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當讀線程的數量大大超過寫線程時,選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里我們可以看到JMM在實現上的一個特點:首先確保正確性,然后再去追求執(zhí)行效率。
下面是在保守策略下,volatile讀插入內存屏障后生成的指令序列示意圖:
上圖中的LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述volatile寫和volatile讀的內存屏障插入策略非常保守。在實際執(zhí)行時,只要不改變volatile寫-讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。下面我們通過具體的示例代碼來說明:
class VolatileBarrierExample { int a; 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寫 } … //其他方法 }
針對readAndWrite()方法,編譯器在生成字節(jié)碼時可以做如下的優(yōu)化:
注意,最后的StoreLoad屏障不能省略。因為第二個volatile寫之后,方法立即return。此時編譯器可能無法準確斷定后面是否會有volatile讀或寫,為了安全起見,編譯器常常會在這里插入一個StoreLoad屏障。
上面的優(yōu)化是針對任意處理器平臺,由于不同的處理器有不同“松緊度”的處理器內存模型,內存屏障的插入還可以根據具體的處理器內存模型繼續(xù)優(yōu)化。以x86處理器為例,上圖中除最后的StoreLoad屏障外,其它的屏障都會被省略。
前面保守策略下的volatile讀和寫,在 x86處理器平臺可以優(yōu)化成:
前文提到過,x86處理器僅會對寫-讀操作做重排序。X86不會對讀-讀,讀-寫和寫-寫操作做重排序,因此在x86處理器中會省略掉這三種操作類型對應的內存屏障。在x86中,JMM僅需在volatile寫后面插入一個StoreLoad屏障即可正確實現volatile寫-讀的內存語義。這意味著在x86處理器中,volatile寫的開銷比volatile讀的開銷會大很多(因為執(zhí)行StoreLoad屏障開銷會比較大)。
二 Volatile的字節(jié)碼為了搞清楚內存屏障,我們扒開class字節(jié)碼看一下,用javap -v -p class文件名(不要.class 后綴)運行
volatile int v1; descriptor: I flags: ACC_VOLATILE ..... void readAndWrite(); descriptor: ()V flags: Code: stack=3, locals=3, args_size=1 0: aload_0 1: getfield #52 // Field v1:I 4: istore_1 5: aload_0 6: getfield #54 // Field v2:I 9: istore_2 10: aload_0 11: iload_1 12: iload_2 13: iadd 14: putfield #72 // Field a:I 17: aload_0 18: iload_1 19: iconst_1 20: isub 21: putfield #52 // Field v1:I 24: aload_0 25: iload_2 26: iload_1 27: imul 28: putfield #54 // Field v2:I 31: return
除了其變量定義的時候有一個Volatile外,之后的字節(jié)碼跟有無Volatile完全一樣,于是我們又扒了下匯編代碼
三 Volatile的匯編碼為了看到匯編碼,要使用hsdis插件, 在mac系統(tǒng)下需要安裝一個hsdis-amd64.dylib的插件。在網上找了一個,地址在這里。
下載下來后,將其放置到你的jre lib目錄下即可。
mac系統(tǒng)上命令如下,
sudo mv ./hsdis-amd64.dylib /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/lib
然后再運行
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite -XX:CompileCommand=compileonly,*VolatileBarrierExample.readAndWrite com.earnfish.VolatileBarrierExample > out.put
其中*VolatileBarrierExample.readAndWrite表示你運行的類.函數, com.earnfish.VolatileBarrierExample表示你的包名.類名,注意需要有main函數來運行你所要執(zhí)行的函數。得出匯編碼如下
0x000000011214bb49: mov %rdi,%rax 0x000000011214bb4c: dec %eax 0x000000011214bb4e: mov %eax,0x10(%rsi) 0x000000011214bb51: lock addl $0x0,(%rsp) ;*putfield v1 ; - com.earnfish.VolatileBarrierExample::readAndWrite@21 (line 35) 0x000000011214bb56: imul %edi,%ebx 0x000000011214bb59: mov %ebx,0x14(%rsi) 0x000000011214bb5c: lock addl $0x0,(%rsp) ;*putfield v2 ; - com.earnfish.VolatileBarrierExample::readAndWrite@28 (line 36)
其對應的Java代碼如下
v1 = i - 1; // 第一個volatile寫 v2 = j * i; // 第二個volatile寫
可見其本質是通過一個lock指令來實現的。那么lock是什么意思呢?
查詢IA32手冊,它的作用是使得本CPU的Cache寫入了內存,該寫入動作也會引起別的CPU invalidate其Cache。所以通過這樣一個空操作,可讓前面volatile變量的修改對其他CPU立即可見。
所以,它的作用是
鎖住主存
任何讀必須在寫完成之后再執(zhí)行
使其它線程這個值的棧緩存失效
類似于前面是storestore,后面是storeload
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.hztianpu.com/yun/69025.html
摘要:內存語義的的實現可見性的實現基于的讀取,寫入兩個操作的內存語義。首先,對中內存屏障的介紹內存屏障用于控制特定條件下的重排序和內存可見性問題。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。 volatile,可見性,有序性 volatile的特性 可見性:對一個volatile變量的讀,總能獲取其他任意線程對該變量最后的寫入。 有序性:JMM會限制volat...
摘要:文章簡介分析的作用以及底層實現原理,這也是大公司喜歡問的問題內容導航的作用什么是可見性源碼分析的作用在多線程中,和都起到非常重要的作用,是通過加鎖來實現線程的安全性。而的主要作用是在多處理器開發(fā)中保證共享變量對于多線程的可見性。 文章簡介 分析volatile的作用以及底層實現原理,這也是大公司喜歡問的問題 內容導航 volatile的作用 什么是可見性 volatile源碼分析 ...
摘要:內存模型基本概念計算機在執(zhí)行程序時,每條指令都是在中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數據的讀取和寫入。有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。 內存模型基本概念 計算機在執(zhí)行程序時,每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數據的讀取和寫入。由于程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由于CPU執(zhí)行速度很快,而從內存讀取數據...
摘要:一言以蔽之,被修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現數據臟讀的現象。為了實現內存語義時,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內存模型告訴我們,各個線程會將共享變量從主內存中拷貝到工作內存,然后執(zhí)行引擎會基于工作內存中的數據進行操作處理。 線程在工作內存進行操作后何時會寫到主內存中...
摘要:一言以蔽之,被修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現數據臟讀的現象。為了實現內存語義時,編譯器在生成字節(jié)碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內存模型告訴我們,各個線程會將共享變量從主內存中拷貝到工作內存,然后執(zhí)行引擎會基于工作內存中的數據進行操作處理。 線程在工作內存進行操作后何時會寫到主內存中...
閱讀 1919·2021-11-22 15:25
閱讀 4157·2021-11-17 09:33
閱讀 2604·2021-10-12 10:12
閱讀 1885·2021-10-09 09:44
閱讀 3305·2021-10-08 10:04
閱讀 1389·2021-09-29 09:35
閱讀 2019·2019-08-30 12:57
閱讀 1377·2019-08-29 16:22