Java關(guān)鍵字volatile知識(shí)點(diǎn)總結(jié)
volatile關(guān)鍵字是Java提供的一種輕量級(jí)同步機(jī)制。它能夠保證可見(jiàn)性和有序性,但是不能保證原子性
可見(jiàn)性對(duì)于volatile的可見(jiàn)性,先看看這段代碼的執(zhí)行

flag默認(rèn)為true 創(chuàng)建一個(gè)線程A去判斷flag是否為true,如果為true循環(huán)執(zhí)行i++操作兩秒后,創(chuàng)建另一個(gè)線程B將flag修改為false 線程A沒(méi)有感知到flag已經(jīng)被修改成false了,不能跳出循環(huán)
這相當(dāng)于啥呢?相當(dāng)于你的女神和你說(shuō),你好好努力,年薪百萬(wàn)了就嫁給你,你聽(tīng)了之后,努力賺錢。3年之后,你年薪百萬(wàn)了,回去找你女神,結(jié)果發(fā)現(xiàn)你女神結(jié)婚了,她結(jié)婚的消息根本沒(méi)有告訴你!難不難受?
女神結(jié)婚可以不告訴你,可是Java代碼中的屬性都是存在內(nèi)存中,一個(gè)線程的修改為什么另一個(gè)線程為什么不可見(jiàn)呢?這就不得不提到Java中的內(nèi)存模型了,Java中的內(nèi)存模型,簡(jiǎn)稱JMM,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系,定義了線程之間的共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存,本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本,它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。
注意!JMM是一個(gè)屏蔽了不同操作系統(tǒng)架構(gòu)的差異的抽象概念,只是一組Java規(guī)范。

了解了JMM,現(xiàn)在我們?cè)倩仡櫼幌挛恼麻_(kāi)頭的那段代碼,為什么線程B修改了flag線程A看到的還是原來(lái)的值呢?

因?yàn)榫€程A復(fù)制了一份剛開(kāi)始的flage=true到本地內(nèi)存,之后線程A使用的flag都是這個(gè)復(fù)制到本地內(nèi)存的flag。線程B修改了flag之后,將flag的值刷新到主內(nèi)存,此時(shí)主內(nèi)存的flag值變成了false。線程A是不知道線程B修改了flag,一直用的是本地內(nèi)存的flag = true。
那么,如何才能讓線程A知道flag被修改了呢?或者說(shuō)怎么讓線程A本地內(nèi)存中緩存的flag無(wú)效,實(shí)現(xiàn)線程間可見(jiàn)呢?用volatile修飾flag就可以做到:

我們可以看到,用volatile修飾flag之后,線程B修改flag之后線程A是能感知到的,說(shuō)明了volatile保證了線程同步之間的可見(jiàn)性。
重排序在闡述volatile有序性之前,需要先補(bǔ)充一些關(guān)于重排序的知識(shí)。
重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。
為什么要有重排序呢?簡(jiǎn)單來(lái)說(shuō),就是為了提升執(zhí)行效率。為什么能提升執(zhí)行效率呢?我們看下面這個(gè)例子:

可以看到重排序之后CPU實(shí)際執(zhí)行省略了一個(gè)讀取和寫回的操作,也就間接的提升了執(zhí)行效率。
有一點(diǎn)必須強(qiáng)調(diào)的是,上圖的例子只是為了讓讀者更好的理解為什么重排序能提升執(zhí)行效率,實(shí)際上Java里面的重排序并不是基于代碼級(jí)別的,從代碼到CPU執(zhí)行之間還有很多個(gè)階段,CPU底層還有一些優(yōu)化,實(shí)際上的執(zhí)行流程可能并不是上圖的說(shuō)的那樣。不必過(guò)于糾結(jié)于此。
重排序可以提高程序的運(yùn)行效率,但是必須遵循as-if-serial語(yǔ)義。as-if-serial語(yǔ)義是什么呢?簡(jiǎn)單來(lái)說(shuō),就是不管你怎么重排序,你必須保證不管怎么重排序,單線程下程序的執(zhí)行結(jié)果不能被改變。
有序性上面我們已經(jīng)介紹了Java有重排序情況,現(xiàn)在我們?cè)賮?lái)聊一聊volatile的有序性。
先看一個(gè)經(jīng)典的面試題:為什么DDL(double check lock)單例模式需要加volatile關(guān)鍵字?

因?yàn)閟ingleton = new Singleton()不是一個(gè)原子操作,大概要經(jīng)過(guò)這幾個(gè)步驟:
分配一塊內(nèi)存空間調(diào)用構(gòu)造器,初始化實(shí)例 singleton指向分配的內(nèi)存空間
實(shí)際執(zhí)行的時(shí)候,可能發(fā)生重排序,導(dǎo)致實(shí)際執(zhí)行步驟是這樣的:
申請(qǐng)一塊內(nèi)存空間 singleton指向分配的內(nèi)存空間調(diào)用構(gòu)造器,初始化實(shí)例
在singleton指向分配的內(nèi)存空間之后,singleton就不為空了。但是在沒(méi)有調(diào)用構(gòu)造器初始化實(shí)例之前,這個(gè)對(duì)象還處于半初始化狀態(tài),在這個(gè)狀態(tài)下,實(shí)例的屬性都還是默認(rèn)屬性,這個(gè)時(shí)候如果有另一個(gè)線程調(diào)用getSingleton()方法時(shí),會(huì)拿到這個(gè)半初始化的對(duì)象,導(dǎo)致出錯(cuò)。
而加volatile修飾之后,就會(huì)禁止重排序,這樣就能保證在對(duì)象初始化完了之后才把singleton指向分配的內(nèi)存空間,杜絕了一些不可控錯(cuò)誤的產(chǎn)生。volatile提供了happens-before保證,對(duì)volatile變量的寫入happens-before所有其他線程后續(xù)對(duì)的讀操作。
原理從上面的DDL單例用例來(lái)看,在并發(fā)情況下,重排序的存在會(huì)導(dǎo)致一些未知的錯(cuò)誤。而加上volatile之后會(huì)防止重排序,那volatile是如何禁止重排序呢?
為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,JMM會(huì)限制特定類型的編譯器和處理器重排序,JMM會(huì)針對(duì)編譯器制定volatile重排序規(guī)則表:

總結(jié)來(lái)說(shuō)就是:
第二個(gè)操作是volatile寫,不管第一個(gè)操作是什么都不會(huì)重排序第一個(gè)操作是volatile讀,不管第二個(gè)操作是什么都不會(huì)重排序第一個(gè)操作是volatile寫,第二個(gè)操作是volatile讀,也不會(huì)發(fā)生重排序
如何保證這些操作不會(huì)發(fā)送重排序呢?就是通過(guò)插入內(nèi)存屏障保證的,JMM層面的內(nèi)存屏障分為讀(load)屏障和寫(Store)屏障,排列組合就有了四種屏障。對(duì)于volatile操作,JMM內(nèi)存屏障插入策略:
在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障

上面的屏障都是JMM規(guī)范級(jí)別的,意思是,按照這個(gè)規(guī)范寫JDK能保證volatile修飾的內(nèi)存區(qū)域的操作不會(huì)發(fā)送重排序。
在硬件層面上,也提供了一系列的內(nèi)存屏障來(lái)提供一致性的能力。拿X86平臺(tái)來(lái)說(shuō),主要提供了這幾種內(nèi)存屏障指令:
lfence指令:在lfence指令前的讀操作當(dāng)必須在lfence指令后的讀操作前完成,類似于讀屏障 sfence指令:在sfence指令前的寫操作當(dāng)必須在sfence指令后的寫操作前完成,類似于寫屏障 mfence指令: 在mfence指令前的讀寫操作當(dāng)必須在mfence指令后的讀寫操作前完成,類似讀寫屏障。
JMM規(guī)范需要加這么多內(nèi)存屏障,但實(shí)際情況并不需要加這么多內(nèi)存屏障。以我們常見(jiàn)的X86處理器為例,X86處理器不會(huì)對(duì)讀-讀、讀-寫和寫-寫操作做重排序,會(huì)省略掉這3種操作類型對(duì)應(yīng)的內(nèi)存屏障,僅會(huì)對(duì)寫-讀操作做重排序。所以volatile寫-讀操作只需要在volatile寫后插入StoreLoad屏障。在《The JSR-133 Cookbook for Compiler Writers》中,也很明確的指出了這一點(diǎn):

而在x86處理器中,有三種方法可以實(shí)現(xiàn)實(shí)現(xiàn)StoreLoad屏障的效果,分別為:
mfence指令:上文提到過(guò),能實(shí)現(xiàn)全能型屏障,具備lfence和sfence的能力。 cpuid指令:cpuid操作碼是一個(gè)面向x86架構(gòu)的處理器補(bǔ)充指令,它的名稱派生自CPU識(shí)別,作用是允許軟件發(fā)現(xiàn)處理器的詳細(xì)信息。 lock指令前綴:總線鎖。lock前綴只能加在一些特殊的指令前面。
實(shí)際上HotSpot關(guān)于volatile的實(shí)現(xiàn)就是使用的lock指令,只在volatile標(biāo)記的地方加上帶lock前綴指令操作,并沒(méi)有參照J(rèn)MM規(guī)范的屏障設(shè)計(jì)而使用對(duì)應(yīng)的mfence指令。
加上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XcompJVM參數(shù)再次執(zhí)行main方法,在打印的匯編碼中,我們也可以看到有一個(gè)lock addl $0x0,(%rsp)的操作。

在源碼中也可以得到驗(yàn)證:

lock addl $0x0,(%rsp)后面的addl $0x0,(%rsp)其實(shí)是一個(gè)空操作。add是加的意思,0x0是16進(jìn)制的0,rsp是一種類型寄存器,合起來(lái)就是把寄存器的值加0,加0是不是等于什么都沒(méi)有做?這段匯編碼僅僅是lock指令的一個(gè)載體而已。其實(shí)上文也有提到過(guò),lock前綴只能加在一些特殊的指令前面,add就是其中一個(gè)指令。
至于Hotspot為什么要使用lock指令而不是mfence指令,按照我的理解,其實(shí)就是省事,實(shí)現(xiàn)起來(lái)簡(jiǎn)單。因?yàn)閘ock功能過(guò)于強(qiáng)大,不需要有太多的考慮。而且lock指令優(yōu)先鎖緩存行,在性能上,lock指令也沒(méi)有想象中的那么差,mfence指令更沒(méi)有想象中的好。所以,使用lock是一個(gè)性價(jià)比非常高的一個(gè)選擇。而且,lock也有對(duì)可見(jiàn)性的語(yǔ)義說(shuō)明。
在《IA-32架構(gòu)軟件開(kāi)發(fā)人員手冊(cè)》的指令表中找到lock:

我不打算在這里深入闡述lock指令的實(shí)現(xiàn)原理和細(xì)節(jié),這很容易陷入堆砌技術(shù)術(shù)語(yǔ)中,而且也超出了本文的范圍,有興趣的可以去看看《IA-32架構(gòu)軟件開(kāi)發(fā)人員手冊(cè)》。
我們只需要知道lock的這幾個(gè)作用就可以了:
確保后續(xù)指令執(zhí)行的原子性。在Pentium及之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會(huì)鎖住總線,使得其它處理器暫時(shí)無(wú)法通過(guò)總線訪問(wèn)內(nèi)存,很顯然,這個(gè)開(kāi)銷很大。在新的處理器中,Intel使用緩存鎖定來(lái)保證指令執(zhí)行的原子性,緩存鎖定將大大降低lock前綴指令的執(zhí)行開(kāi)銷。禁止該指令與前面和后面的讀寫指令重排序。把寫緩沖區(qū)的所有數(shù)據(jù)刷新到內(nèi)存中。
總結(jié)來(lái)說(shuō),就是lock指令既保證了可見(jiàn)性也保證了原子性。
重要的事情再說(shuō)一遍,是lock指令既保證了可見(jiàn)性也保證了原子性,和什么緩沖一致性協(xié)議啊,MESI什么的沒(méi)有一點(diǎn)關(guān)系。
為了不讓你把緩存一致性協(xié)議和JMM混淆,在前面的文章中,我特意沒(méi)有提到過(guò)緩存一致性協(xié)議,因?yàn)檫@兩者本不是一個(gè)維度的東西,存在的意義也不一樣,這一部分,我們下次再聊。
總結(jié)全文重點(diǎn)是圍繞volatile的可見(jiàn)性和有序性展開(kāi)的,其中花了不少的部分篇幅描述了一些計(jì)算機(jī)底層的概念,對(duì)于讀者來(lái)說(shuō)可能過(guò)于無(wú)趣,但如果你能認(rèn)真看完,我相信你或多或少也會(huì)有一點(diǎn)收獲。
不去深究,volatile只是一個(gè)普通的關(guān)鍵字。深入探討,你會(huì)發(fā)現(xiàn)volatile是一個(gè)非常重要的知識(shí)點(diǎn)。volatile能將軟件和硬件結(jié)合起來(lái),想要徹底弄懂,需要深入到計(jì)算機(jī)的最底層。但如果你做到了。你對(duì)Java的認(rèn)知一定會(huì)有進(jìn)一步的提升。
只把眼光放在Java語(yǔ)言,似乎顯得非常局限。發(fā)散到其他語(yǔ)言,C語(yǔ)言,C++里面也都有volatile關(guān)鍵字。我沒(méi)有看過(guò)C語(yǔ)言,C++里面volatile關(guān)鍵字是如何實(shí)現(xiàn)的,但我相信底層的原理一定是相通的。
到此這篇關(guān)于Java關(guān)鍵字volatile知識(shí)點(diǎn)總結(jié)的文章就介紹到這了,更多相關(guān)理解Java關(guān)鍵字volatile內(nèi)容請(qǐng)搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. 基于PHP做個(gè)圖片防盜鏈2. php使用正則驗(yàn)證密碼字段的復(fù)雜強(qiáng)度原理詳細(xì)講解 原創(chuàng)3. ASP.NET MVC使用Boostrap實(shí)現(xiàn)產(chǎn)品展示、查詢、排序、分頁(yè)4. XML在語(yǔ)音合成中的應(yīng)用5. jscript與vbscript 操作XML元素屬性的代碼6. asp.net core 認(rèn)證和授權(quán)實(shí)例詳解7. ASP.NET MVC把數(shù)據(jù)庫(kù)中枚舉項(xiàng)的數(shù)字轉(zhuǎn)換成文字8. 如何使用ASP.NET Core 配置文件9. .NET中實(shí)現(xiàn)對(duì)象數(shù)據(jù)映射示例詳解10. 基于javaweb+jsp實(shí)現(xiàn)企業(yè)車輛管理系統(tǒng)
