摘要:為了實現(xiàn)的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。上述寫和讀的內(nèi)存屏障插入策略非常保守。
本講座地址https://segmentfault.com/l/15... 歡迎大家圍觀
Java的Volatile的特征是任何讀都能讀到最新值,本質(zhì)上是JVM通過內(nèi)存屏障來實現(xiàn)的,讓我們看看從字節(jié)碼以及匯編碼的角度,來看下是否真是如此?
一 Volatile與內(nèi)存屏障本節(jié)內(nèi)容來自:http://www.infoq.com/cn/artic...
為了實現(xiàn)volatile內(nèi)存語義,JMM會分別限制重排序類型。下面是JMM針對編譯器制定的volatile重排序規(guī)則表:
舉例來說,第三行最后一個單元格的意思是:在程序順序中,當(dāng)?shù)谝粋€操作為普通變量的讀或?qū)憰r,如果第二個操作為volatile寫,則編譯器不能重排序這兩個操作。
從上表我們可以看出:
當(dāng)?shù)诙€操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規(guī)則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
當(dāng)?shù)谝粋€操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規(guī)則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
當(dāng)?shù)谝粋€操作是volatile寫,第二個操作是volatile讀時,不能重排序。
為了實現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能,為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的后面插入一個StoreLoad屏障。
在每個volatile讀操作的后面插入一個LoadLoad屏障。
在每個volatile讀操作的后面插入一個LoadStore屏障。
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內(nèi)存語義。
下面是保守策略下,volatile寫插入內(nèi)存屏障后生成的指令序列示意圖:
上圖中的StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對任意處理器可見了。這是因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
這里比較有意思的是volatile寫后面的StoreLoad屏障。這個屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因為編譯器常常無法準(zhǔn)確判斷在一個volatile寫的后面,是否需要插入一個StoreLoad屏障(比如,一個volatile寫之后方法立即return)。為了保證能正確實現(xiàn)volatile的內(nèi)存語義,JMM在這里采取了保守策略:在每個volatile寫的后面或在每個volatile讀的前面插入一個StoreLoad屏障。從整體執(zhí)行效率的角度考慮,JMM選擇了在每個volatile寫的后面插入一個StoreLoad屏障。因為volatile寫-讀內(nèi)存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當(dāng)讀線程的數(shù)量大大超過寫線程時,選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里我們可以看到JMM在實現(xiàn)上的一個特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率。
下面是在保守策略下,volatile讀插入內(nèi)存屏障后生成的指令序列示意圖:
上圖中的LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。
上述volatile寫和volatile讀的內(nèi)存屏障插入策略非常保守。在實際執(zhí)行時,只要不改變volatile寫-讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。下面我們通過具體的示例代碼來說明:
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。此時編譯器可能無法準(zhǔn)確斷定后面是否會有volatile讀或?qū)懀瑸榱税踩鹨?,編譯器常常會在這里插入一個StoreLoad屏障。
上面的優(yōu)化是針對任意處理器平臺,由于不同的處理器有不同“松緊度”的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。以x86處理器為例,上圖中除最后的StoreLoad屏障外,其它的屏障都會被省略。
前面保守策略下的volatile讀和寫,在 x86處理器平臺可以優(yōu)化成:
前文提到過,x86處理器僅會對寫-讀操作做重排序。X86不會對讀-讀,讀-寫和寫-寫操作做重排序,因此在x86處理器中會省略掉這三種操作類型對應(yīng)的內(nèi)存屏障。在x86中,JMM僅需在volatile寫后面插入一個StoreLoad屏障即可正確實現(xiàn)volatile寫-讀的內(nèi)存語義。這意味著在x86處理器中,volatile寫的開銷比volatile讀的開銷會大很多(因為執(zhí)行StoreLoad屏障開銷會比較大)。
二 Volatile的字節(jié)碼為了搞清楚內(nèi)存屏障,我們扒開class字節(jié)碼看一下,用javap -v -p class文件名(不要.class 后綴)運(yùn)行
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的插件。在網(wǎng)上找了一個,地址在這里。
下載下來后,將其放置到你的jre lib目錄下即可。
mac系統(tǒng)上命令如下,
sudo mv ./hsdis-amd64.dylib /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/lib
然后再運(yùn)行
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite -XX:CompileCommand=compileonly,*VolatileBarrierExample.readAndWrite com.earnfish.VolatileBarrierExample > out.put
其中*VolatileBarrierExample.readAndWrite表示你運(yùn)行的類.函數(shù), com.earnfish.VolatileBarrierExample表示你的包名.類名,注意需要有main函數(shù)來運(yùn)行你所要執(zhí)行的函數(shù)。得出匯編碼如下
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)
其對應(yīng)的Java代碼如下
v1 = i - 1; // 第一個volatile寫 v2 = j * i; // 第二個volatile寫
可見其本質(zhì)是通過一個lock指令來實現(xiàn)的。那么lock是什么意思呢?
查詢IA32手冊,它的作用是使得本CPU的Cache寫入了內(nèi)存,該寫入動作也會引起別的CPU invalidate其Cache。所以通過這樣一個空操作,可讓前面volatile變量的修改對其他CPU立即可見。
所以,它的作用是
鎖住主存
任何讀必須在寫完成之后再執(zhí)行
使其它線程這個值的棧緩存失效
類似于前面是storestore,后面是storeload
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/69025.html
摘要:內(nèi)存語義的的實現(xiàn)可見性的實現(xiàn)基于的讀取,寫入兩個操作的內(nèi)存語義。首先,對中內(nèi)存屏障的介紹內(nèi)存屏障用于控制特定條件下的重排序和內(nèi)存可見性問題。在大多數(shù)處理器的實現(xiàn)中,這個屏障是個萬能屏障,兼具其它三種內(nèi)存屏障的功能。 volatile,可見性,有序性 volatile的特性 可見性:對一個volatile變量的讀,總能獲取其他任意線程對該變量最后的寫入。 有序性:JMM會限制volat...
摘要:文章簡介分析的作用以及底層實現(xiàn)原理,這也是大公司喜歡問的問題內(nèi)容導(dǎo)航的作用什么是可見性源碼分析的作用在多線程中,和都起到非常重要的作用,是通過加鎖來實現(xiàn)線程的安全性。而的主要作用是在多處理器開發(fā)中保證共享變量對于多線程的可見性。 文章簡介 分析volatile的作用以及底層實現(xiàn)原理,這也是大公司喜歡問的問題 內(nèi)容導(dǎo)航 volatile的作用 什么是可見性 volatile源碼分析 ...
摘要:內(nèi)存模型基本概念計算機(jī)在執(zhí)行程序時,每條指令都是在中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入。有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。 內(nèi)存模型基本概念 計算機(jī)在執(zhí)行程序時,每條指令都是在CPU中執(zhí)行的,而執(zhí)行指令過程中,勢必涉及到數(shù)據(jù)的讀取和寫入。由于程序運(yùn)行過程中的臨時數(shù)據(jù)是存放在主存(物理內(nèi)存)當(dāng)中的,這時就存在一個問題,由于CPU執(zhí)行速度很快,而從內(nèi)存讀取數(shù)據(jù)...
摘要:一言以蔽之,被修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。為了實現(xiàn)內(nèi)存語義時,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內(nèi)存模型告訴我們,各個線程會將共享變量從主內(nèi)存中拷貝到工作內(nèi)存,然后執(zhí)行引擎會基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理。 線程在工作內(nèi)存進(jìn)行操作后何時會寫到主內(nèi)存中...
摘要:一言以蔽之,被修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現(xiàn)數(shù)據(jù)臟讀的現(xiàn)象。為了實現(xiàn)內(nèi)存語義時,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內(nèi)存模型告訴我們,各個線程會將共享變量從主內(nèi)存中拷貝到工作內(nèi)存,然后執(zhí)行引擎會基于工作內(nèi)存中的數(shù)據(jù)進(jìn)行操作處理。 線程在工作內(nèi)存進(jìn)行操作后何時會寫到主內(nèi)存中...
閱讀 1857·2021-11-22 15:25
閱讀 3950·2021-11-17 09:33
閱讀 2523·2021-10-12 10:12
閱讀 1811·2021-10-09 09:44
閱讀 3241·2021-10-08 10:04
閱讀 1325·2021-09-29 09:35
閱讀 1959·2019-08-30 12:57
閱讀 1312·2019-08-29 16:22