volatile底層實(shí)現(xiàn)原理

前言

當(dāng)共享變量被聲明為volatile后,對這個(gè)變量的讀/寫操作都會(huì)很特別,下面我們就揭開volatile的神秘面紗。

1.volatile的內(nèi)存語義

1.1 volatile的特性

一個(gè)volatile變量自身具有以下三個(gè)特性:

  1. 可見性:即當(dāng)一個(gè)線程修改了聲明為volatile變量的值,新值對于其他要讀該變量的線程來說是立即可見的。而普通變量是不能做到這一點(diǎn)的,普通變量的值在線程間傳遞需要通過主內(nèi)存來完成。
  1. 有序性:volatile變量的所謂有序性也就是被聲明為volatile的變量的臨界區(qū)代碼的執(zhí)行是有順序的,即禁止指令重排序。
  1. 受限原子性:這里volatile變量的原子性與synchronized的原子性是不同的,synchronized的原子性是指只要聲明為synchronized的方法或代碼塊兒在執(zhí)行上就是原子操作的。而volatile是不修飾方法或代碼塊兒的,它用來修飾變量,對于單個(gè)volatile變量的讀/寫操作都具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。所以volatile的原子性是受限制的。并且在多線程環(huán)境中,volatile并不能保證原子性。
1.2 volatile寫-讀的內(nèi)存語義

volatile寫的內(nèi)存語義:當(dāng)寫線程寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。

volatile讀的內(nèi)存語義:當(dāng)讀線程讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對應(yīng)的本地內(nèi)存置為無效,線程接下來將從主內(nèi)存讀取共享變量。

2.volatile語義實(shí)現(xiàn)原理

在介紹volatile語義實(shí)現(xiàn)原理之前,我們先來看兩個(gè)與CPU相關(guān)的專業(yè)術(shù)語:

  • 內(nèi)存屏障(memory barriers):一組處理器指令,用于實(shí)現(xiàn)對內(nèi)存操作的順序限制。
  • 緩存行(cache line):CPU高速緩存中可以分配的最小存儲(chǔ)單位。處理器填寫緩存行時(shí)會(huì)加載整個(gè)緩存行。
2.1 volatile可見性實(shí)現(xiàn)原理

volatile可見性的內(nèi)存語義是如何實(shí)現(xiàn)的呢?下面我們看一段代碼,并將代碼生成的處理器的匯編指令打印出來(關(guān)于如何打印匯編指令,我會(huì)在文章末尾附上),看下對volatile變量進(jìn)行寫操作時(shí),CPU會(huì)做什么事情:

public class VolatileTest {

    private static volatile VolatileTest instance = null;

    private VolatileTest(){}

    public static VolatileTest getInstance(){
        if(instance == null){
            instance = new VolatileTest();
        }

        return instance;
    }

    public static void main(String[] args) {
        VolatileTest.getInstance();
    }
}


以上的代碼是一個(gè)我們非常熟悉的在多線程環(huán)境中不能保證線程安全的單例模式代碼,這段代碼中特殊的地方是,我將實(shí)例變量instance加上了volatile修飾,下面看打印的匯編指令:

image.png

上面截圖中,我們看到我劃線的一行的末尾有一句匯編注釋:putstatic instance,了解JVM 字節(jié)碼指令的小伙伴都知道,putstatic的含義是給一個(gè)靜態(tài)變量設(shè)置值,在上述代碼中也就是給靜態(tài)變量instance賦值,對應(yīng)代碼:instance = new VolatileTest();在getInstance方法中為instance實(shí)例化,因?yàn)閕nstance加了volatile修飾,所以給靜態(tài)變量instance設(shè)置值也是在寫一個(gè)volatile變量。

看到上述有匯編指令,也有字節(jié)碼指令,大家會(huì)不會(huì)混淆這兩種指令,這里我指明一下字節(jié)碼指令和匯編指令的區(qū)別:

我們都知道java是一種跨平臺(tái)的語言,那么java是如何實(shí)現(xiàn)這種平臺(tái)無關(guān)性的呢?這就需要我們了解JVM和java的字節(jié)碼文件。這里我們需要有一點(diǎn)共識(shí),就是任何一門編程語言都需要轉(zhuǎn)換為與平臺(tái)相關(guān)的匯編指令才能夠最終被硬件執(zhí)行,比如C和C++都將我們的源代碼直接編譯成與CPU相關(guān)的匯編指令給CPU執(zhí)行。 不同系列的CPU的體系架構(gòu)不同,所以它們的匯編指令也有不同,比如X86架構(gòu)的CPU對應(yīng)于X86匯編指令,arm架構(gòu)的CPU對應(yīng)于arm匯編指令。如果將程序源代碼直接編譯成與硬件相關(guān)的底層匯編指令,那么程序的跨平臺(tái)性也就大打折扣,但執(zhí)行性能相對較高。為了實(shí)現(xiàn)平臺(tái)無關(guān)性,java的編譯器javac并不是將java的源程序直接編譯成與平臺(tái)相關(guān)的匯編指令,而是編譯成一種中間語言,即java的class字節(jié)碼文件。字節(jié)碼文件,顧名思義存的就是字節(jié)碼,即一個(gè)一個(gè)的字節(jié)。有打開過java字節(jié)碼文件研讀過的小伙伴可能會(huì)發(fā)現(xiàn),字節(jié)碼文件里面存的并不是二進(jìn)制,而是十六進(jìn)制,這是因?yàn)槎M(jìn)制太長了,一個(gè)字節(jié)要由8位二進(jìn)制組成。所以用十六進(jìn)制表示,兩個(gè)十六進(jìn)制就可以表示一個(gè)字節(jié)。java源碼編譯后的字節(jié)碼文件是不能夠直接被CPU執(zhí)行的,那么該如何執(zhí)行呢?答案是JVM,為了讓java程序能夠在不同的平臺(tái)上執(zhí)行,java官方提供了針對于各個(gè)平臺(tái)的java虛擬機(jī),JVM運(yùn)行于硬件層之上,屏蔽各種平臺(tái)的差異性。javac編譯后的字節(jié)碼文件統(tǒng)一由JVM來加載,最后再轉(zhuǎn)化成與硬件相關(guān)的機(jī)器指令被CPU執(zhí)行。知道了通過JVM來加載字節(jié)碼文件,那么還有一個(gè)問題,就是JVM如何將字節(jié)碼中的每個(gè)字節(jié)和我們寫的java源代碼相關(guān)聯(lián),也就是JVM如何知道我們寫的java源代碼對應(yīng)于class文件中的哪段十六進(jìn)制,這段十六進(jìn)制是干什么的,執(zhí)行了什么功能?并且一大堆的十六進(jìn)制,我們也看不懂啊。所以這就需要定義一個(gè)JVM層面的規(guī)范,在JVM層面抽象出一些我們能夠認(rèn)識(shí)的指令助記符,這些指令助記符就是java的字節(jié)碼指令。

再看上面的截圖,當(dāng)寫instance這個(gè)volatile變量時(shí),發(fā)現(xiàn)add前面加個(gè)一個(gè)lock指令,我在截圖中框了出來,如何不加volatile修飾,是沒有l(wèi)ock的。

lock指令在多核處理器下會(huì)引發(fā)下面的事件:

將當(dāng)前處理器的緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存,同時(shí)使其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)置為無效。

為了提高處理速度,處理器一般不直接和內(nèi)存通信,而是先將系統(tǒng)內(nèi)存的數(shù)據(jù)讀到內(nèi)部緩存后再進(jìn)行操作,但操作完成后并不知道處理器何時(shí)將緩存數(shù)據(jù)寫回到內(nèi)存。但如果對加了volatile修飾的變量進(jìn)行寫操作,JVM就會(huì)向處理器發(fā)送一條lock前綴的指令,將這個(gè)變量在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。這時(shí)只是寫回到系統(tǒng)內(nèi)存,但其他處理器的緩存行中的數(shù)據(jù)還是舊的,要使其他處理器緩存行的數(shù)據(jù)也是新寫回的系統(tǒng)內(nèi)存的數(shù)據(jù),就需要實(shí)現(xiàn)緩存一致性協(xié)議。即在一個(gè)處理器將自己緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存后,其他的每個(gè)處理器就會(huì)通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的數(shù)據(jù)是否已過期,當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址的數(shù)據(jù)被修改后,就會(huì)將自己緩存行緩存的數(shù)據(jù)設(shè)置為無效,當(dāng)處理器要對這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀取到自己的緩存行,重新緩存。

總結(jié)下:volatile可見性的實(shí)現(xiàn)就是借助了CPU的lock指令,通過在寫volatile的機(jī)器指令前加上lock前綴,使寫volatile具有以下兩個(gè)原則:

  1. 寫volatile時(shí)處理器會(huì)將緩存寫回到主內(nèi)存。
  2. 一個(gè)處理器的緩存寫回到內(nèi)存會(huì)導(dǎo)致其他處理器的緩存失效。
2.2 volatile有序性的實(shí)現(xiàn)原理

volatile有序性的保證就是通過禁止指令重排序來實(shí)現(xiàn)的。指令重排序包括編譯器和處理器重排序,JMM會(huì)分別限制這兩種指令重排序。

那么禁止指令重排序又是如何實(shí)現(xiàn)的呢?答案是加內(nèi)存屏障。JMM為volatile加內(nèi)存屏障有以下4種情況:

  1. 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障,防止寫volatile與后面的寫操作重排序。
  2. 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障,防止寫volatile與后面的讀操作重排序。
  3. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障,防止讀volatile與后面的讀操作重排序。
  4. 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障,防止讀volatile與后面的寫操作重排序。

上述內(nèi)存屏障的插入策略是非常保守的,比如一個(gè)volatile的寫操作后面需要加上StoreStore和StoreLoad屏障,但這個(gè)寫volatile后面可能并沒有讀操作,因此理論上只加上StoreStore屏障就可以,的確,有的處理器就是這么做的。但JMM這種保守的內(nèi)存屏障插入策略能夠保證在任意的處理器平臺(tái),volatile變量都是有序的。

3.JSR-133對volatile內(nèi)存語義的增強(qiáng)

在JSR-133之前的舊的java內(nèi)存模型中,雖然不允許對volatile變量之間的操作進(jìn)行重排序,但允許對volatile變量與普通變量之間進(jìn)行重排序。比如內(nèi)存屏障前面是一個(gè)寫volatile變量的操作,內(nèi)存屏障后面的操作是一個(gè)寫普通變量的操作,即使這兩個(gè)寫操作可能會(huì)破壞volatile內(nèi)存語義,但JMM是允許這兩個(gè)操作進(jìn)行重排序的。

在JSR-133以及后面的新的java內(nèi)存模型中,增強(qiáng)了volatile的內(nèi)存語義。只要volatile變量與普通變量之間的重排序可能會(huì)破壞volatile的內(nèi)存語義,這種重排序就會(huì)被編譯器重排序規(guī)則和處理器內(nèi)存屏障出入策略禁止。

附:配置idea打印匯編指令

工具包下載地址:鏈接:https://pan.baidu.com/s/11yRnsOHca5EVRfE9gAuVxA
提取碼:gn8z

將下載的工具包解壓,復(fù)制到j(luò)dk安裝目錄的jre路徑下的bin目錄中,如圖:
?

image.png

然后配置idea,在 VM options 選項(xiàng)中輸入:-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*類名.方法名

JRE選項(xiàng)選擇已放入工具包的jre路徑。

下圖是我的idea配置:

image.png

以上配置好后運(yùn)行就可以打印匯編指令了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,204評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內(nèi)容