volatile的幾個靈魂拷問

要想講清楚volatile關(guān)鍵字,這時候就應(yīng)該主動從內(nèi)存模型開始講起,然后說原子性、可見性、有序性的理解,鋪墊好這些才是到volatile關(guān)鍵字的原理,假定前面一篇內(nèi)存模型的文章已經(jīng)理解到位了,那么這里直接從volatile的原理開搞!

首先說結(jié)論,volatile關(guān)鍵字作用的是保證可見性和有序性,并不保證原子性。只有synchronized關(guān)鍵字同時保證上述三種特性。但是volatile是輕量級的synchronized,它在多線程中保證了變量的“可見性”??梢娦缘囊馑际钱斠粋€線程修改了一個變量的值后,另外的線程能夠讀取到這個變量修改后的值。

1、保證可見性

用 volatile 關(guān)鍵字修飾的共享變量,編譯成字節(jié)碼后增加 Lock 前綴指令,該指令要做兩件事:

  • 將當前工作內(nèi)存緩存行的數(shù)據(jù)立即寫回到主內(nèi)存。
  • 寫回主內(nèi)存的操作會使其他工作內(nèi)存里緩存了該共享變量地址的數(shù)據(jù)無效(緩存一致性協(xié)議保證的操作)。

對volatile修飾的變量,執(zhí)行寫操作的話,JVM會發(fā)送一條lock前綴指令給CPU,CPU在計算完之后會立即將這個值寫回主內(nèi)存,同時因為有MESI緩存一致性協(xié)議,所以各個CPU都會對總線進行嗅探,自己本地緩存中的數(shù)據(jù)是否被別人修改。

如果發(fā)現(xiàn)別人修改了某個緩存的數(shù)據(jù),那么CPU就會將自己本地緩存的數(shù)據(jù)過期掉,然后這個CPU上執(zhí)行的線程在讀取那個變量的時候,就會從主內(nèi)存重新加載最新的數(shù)據(jù)了。

所以,volatile的可見性就是:lock前綴指令 + MESI緩存一致性協(xié)議

Java程序打印成匯編語句,引用自 https://zhuanlan.zhihu.com/p/77085695

2、保證有序性

Lock 前綴指令有內(nèi)存屏障的作用。上一篇文章已經(jīng)提到過了,JMM一共有4種內(nèi)存屏障,分別是 LoadLoad、LoadStore、StoreStore、StoreLoad。JMM 給volatile插入內(nèi)存屏障保守策略:

  • 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障,后面插入一個 StoreLoad 屏障。
  • 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障和一個 LoadStore 屏障。
linux系統(tǒng)下x86架構(gòu)的實現(xiàn)

StoreStore屏障可以保證在volatile寫之前,前面所有的普通讀寫操作同步到主內(nèi)存中
StoreLoad屏障可以保證防止前面的volatile寫和后面有可能出現(xiàn)的volatile度/寫進行重排序
LoadLoad屏障可以保證防止下面的普通讀操作和上面的volatile讀進行重排序
LoadStore屏障可以保存防止下面的普通寫操作和上面的volatile讀進行重排序

3、不具備原子性

volatile不具備原子性,多個線程去寫同一個公共volatile修飾的變量會出現(xiàn)線程安全問題,對于64位的long和double,如果沒有被volatile修飾,那么對其操作可以不是原子的。在操作的時候,可以分成兩步,每次對32位操作。如果使用volatile修飾long和double,那么其讀寫都是原子操作,這里可以稍稍注意下。

由于volatile不具備原子性,因此還需要有一個原子性能力的補充,這里就可以看看這個文章進行回顧下:《CAS是什么?Atomic包知多少?》

4、和Synchronized的內(nèi)存屏障差別

按照可見性保障來劃分。內(nèi)存屏障可分為加載屏障(Load Barrier)和存儲屏障(Store Barrier)。加載屏障的作用是刷新處理器緩存,存儲屏障的作用沖刷處理器緩存。Java虛擬機會在 MonitorExit ( 釋放鎖 ) 對應(yīng)的機器碼指令之后插入一個存儲屏障,這就保障了寫線程在釋放鎖之前在臨界區(qū)中對共享變量所做的更新對讀線程的執(zhí)行處理器來說是可同步的。相應(yīng)地,Java 虛擬機會在 MonitorEnter ( 申請鎖 ) 對應(yīng)的機器碼指令之后臨界區(qū)開始之前的地方插入一個加載屏障,這使得讀線程的執(zhí)行處理器能夠?qū)懢€程對相應(yīng)共享變量所做的更新從其他處理器同步到該處理器的高速緩存中。因此,可見性的保障是通過寫線程和讀線程成對地使用存儲屏障和加載屏障實現(xiàn)的。

按照有序性保障來劃分,內(nèi)存屏障可以分為獲取屏障(Acquire Barrier)和釋放屏障 ( Release Barrier )。獲 取 屏 障 的 使 用 方 式 是 在 一 個 讀 操 作 ( 包括 Read-Modify-Write 以及普通的讀操作 )之后插入該內(nèi)存屏障,其作用是禁止該讀操作與其后的任何讀寫操作之間進行重排序,這相當于在進行后續(xù)操作之前先要獲得相應(yīng)共享數(shù)據(jù)的所有權(quán) ( 這也是該屏障的名稱來源 )。釋放屏障的使用方式是在一個寫操作之前插入該內(nèi)存屏障,其作用是禁止該寫操作與其前面的任何讀寫操作之間進行重排序。這相當于在對相應(yīng)共享數(shù)據(jù)操作結(jié)束后釋放所有權(quán)( 這也是該屏障的名稱來源 )。 Java虛擬機會在 MonitorEnter( 它包含了讀操作 ) 對應(yīng)的機器碼指令之后臨界區(qū)開始之前的地方插入一個獲取屏障,并在臨界區(qū)結(jié)束之后 MonitorExit ( 它包含了寫操作 ) 對應(yīng)的機器碼指令之前的地方插入一個釋放屏障。因此,這兩種屏障就像是三明治的兩層面包片把火腿夾住一樣把臨界區(qū)中的代碼(指令序列)包括起來:

可以發(fā)現(xiàn),與volatile類似,synchronized底層也是通過釋放屏障和獲取屏障的配對使用保障有序性,加載屏障和存儲屏障的配對使用保障可見性。最后又通過鎖的排他性保障了原子性。

參考文獻

1、volatile底層原理詳解
2、synchronized 實現(xiàn)原理與內(nèi)存屏障

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容