一次面試經(jīng)歷:
面試官:請(qǐng)講一下 volatile湿酸。
我:volatile 是 java 虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制辜御,當(dāng)變量定義為 volatile 后伍派,可以保證此變量對(duì)多線程的可見(jiàn)性江耀。多個(gè)線程可以讀到內(nèi)存中最新的值。
面試管:volatile 底層具體怎么實(shí)現(xiàn)的诉植? 怎么保證的可見(jiàn)性祥国?
我:。晾腔。舌稀。。
面試官:volatile 怎么保證多線程可以讀到最新的值灼擂?
我:壁查。。剔应。睡腿。
面試結(jié)果可想而知了,面試官隨便問(wèn)了問(wèn)就送我出門(mén)了领斥。
所以打算好好研究一下嫉到。
先來(lái)介紹幾個(gè)知識(shí)點(diǎn):
Java 內(nèi)存模型
主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量?jī)?chǔ)存到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)月洛。注意何恶,這里的變量是指實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素嚼黔,不包括局部變量與方法參數(shù)细层,因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享唬涧,就不存在競(jìng)爭(zhēng)問(wèn)題疫赎。
Java 內(nèi)存模型有主內(nèi)存和工作內(nèi)存,工作內(nèi)存保存了被該線程使用到的變量的主內(nèi)存副本拷貝碎节,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行捧搞,而不能直接讀寫(xiě)主內(nèi)存中的變量,不同線程之間無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存的變量狮荔,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成胎撇。
注意:
當(dāng)一個(gè)變量被volatile修飾后,JMM 會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存殖氏。表示著線程工作內(nèi)存無(wú)效晚树,當(dāng)一個(gè)線程修改共享變量后他會(huì)立即被更新到主內(nèi)存中,當(dāng)其他線程讀取共享變量時(shí)雅采,它會(huì)直接從主內(nèi)存中讀取爵憎。
指令重排序
在執(zhí)行程序時(shí)為了提高性能慨亲,編譯器和處理器通常會(huì)對(duì)指令做重排序:
1.編譯器重排序。編譯器在不改變單線程程序語(yǔ)義的前提下宝鼓,可以重新安排語(yǔ)句的執(zhí)行順序刑棵;
2.處理器重排序。如果不存在數(shù)據(jù)依賴性愚铡,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序铐望;
指令重排序?qū)尉€程沒(méi)有什么影響,他不會(huì)影響程序的運(yùn)行結(jié)果茂附,但是會(huì)影響多線程的正確性,既然指令重排序會(huì)影響到多線程執(zhí)行的正確性督弓,那么我們就需要禁止重排序营曼。那么JVM是如何禁止重排序的呢?這個(gè)問(wèn)題稍后回答愚隧,我們先看另一個(gè)原則happens-before蒂阱,happen-before原則保證了程序的“有序性”,它規(guī)定如果兩個(gè)操作的執(zhí)行順序無(wú)法從happens-before原則中推到出來(lái)狂塘,那么他們就不能保證有序性录煤,可以隨意進(jìn)行重排序。其定義如下:
1.同一個(gè)線程中的荞胡,前面的操作 happen-before 后續(xù)的操作妈踊。(即單線程內(nèi)按代碼順序執(zhí)行。但是泪漂,在不影響在單線程環(huán)境執(zhí)行結(jié)果的前提下廊营,編譯器和處理器可以進(jìn)行重排序,這是合法的萝勤。換句話說(shuō)露筒,這一是規(guī)則無(wú)法保證編譯重排和指令重排)。
2.監(jiān)視器上的解鎖操作 happen-before 其后續(xù)的加鎖操作敌卓。(Synchronized 規(guī)則)慎式。
3.對(duì)volatile變量的寫(xiě)操作 happen-before 后續(xù)的讀操作。(volatile 規(guī)則)趟径。
4.線程的start() 方法 happen-before 該線程所有的后續(xù)操作瘪吏。(線程啟動(dòng)規(guī)則)。
5.線程所有的操作 happen-before 其他線程在該線程上調(diào)用 join 返回成功后的操作舵抹。
6.如果 a happen-before b肪虎,b happen-before c,則a happen-before c(傳遞性)惧蛹。
我們著重看第三點(diǎn)volatile規(guī)則:對(duì)volatile變量的寫(xiě)操作 happen-before 后續(xù)的讀操作扇救。為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義刑枝,JMM會(huì)重排序,
注意
觀察加入volatile關(guān)鍵字和沒(méi)有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn)迅腔,加入volatile關(guān)鍵字時(shí)装畅,會(huì)多出一個(gè)lock前綴指令。lock前綴指令其實(shí)就相當(dāng)于一個(gè)內(nèi)存屏障沧烈。內(nèi)存屏障是一組處理指令掠兄,用來(lái)實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。volatile的底層就是通過(guò)內(nèi)存屏障來(lái)實(shí)現(xiàn)的锌雀。
內(nèi)存屏障
為了保證內(nèi)存可見(jiàn)性蚂夕,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來(lái)禁止特定類(lèi)型的處理器重排序。JMM把內(nèi)存屏障指令分為4類(lèi)腋逆,如下表:
volatile內(nèi)存語(yǔ)義實(shí)現(xiàn)
為了實(shí)現(xiàn) volatile 的內(nèi)存語(yǔ)義婿牍,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型的處理器重排序惩歉。下面是基于保守策略的 JMM 內(nèi)存屏障插入策略:
- 在每個(gè) volatile 寫(xiě)操作的前面插入一個(gè) StoreStore 屏障(禁止前面的寫(xiě)與volatile寫(xiě)重排序)等脂。
- 在每個(gè) volatile 寫(xiě)操作的后面插入一個(gè) StoreLoad 屏障(禁止volatile寫(xiě)與后面可能有的讀和寫(xiě)重排序)。
- 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadLoad 屏障(禁止volatile讀與后面的讀操作重排序)撑蚌。
- 在每個(gè) volatile 讀操作的后面插入一個(gè) LoadStore 屏障(禁止volatile讀與后面的寫(xiě)操作重排序)上遥。
其中重點(diǎn)說(shuō)下StoreLaod屏障,它是確闭浚可見(jiàn)性的關(guān)鍵粉楚,因?yàn)樗鼤?huì)將屏障之前的寫(xiě)緩沖區(qū)中的數(shù)據(jù)全部刷新到主內(nèi)存中。上述內(nèi)存屏障插入策略非常保守第煮,但它可以保證在任意處理平臺(tái)解幼,任意的程序中都能得到正確的volatile語(yǔ)義。下面是保守策略(為什么說(shuō)保守呢包警,因?yàn)橛行┰趯?shí)際的場(chǎng)景是可省略的)下撵摆,volatile 寫(xiě)操作 插入內(nèi)存屏障后生成的指令序列示意圖:
其中StoreStore屏障可以保證在volatile寫(xiě)之前,其前面的所有普通寫(xiě)操作對(duì)任意處理器可見(jiàn)(把它刷新到主內(nèi)存)害晦。另外volatile寫(xiě)后面有StoreLoad屏障特铝,此屏障的作用是避免volatile寫(xiě)與后面可能有的讀或?qū)懖僮鬟M(jìn)行重排序。因?yàn)榫幾g器常常無(wú)法準(zhǔn)確判斷在一個(gè)volatile寫(xiě)的后面是否需要插入一個(gè)StoreLoad屏障(比如壹瘟,一個(gè)volatile寫(xiě)之后方法立即return)為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義鲫剿,JMM采取了保守策略:在每個(gè)volatile寫(xiě)的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫(xiě)-讀內(nèi)存語(yǔ)義的常見(jiàn)模式是:一個(gè)寫(xiě)線程寫(xiě)volatile變量稻轨,多個(gè)度線程讀同一個(gè)volatile變量灵莲。當(dāng)讀線程的數(shù)量大大超過(guò)寫(xiě)線程時(shí),選擇在volatile寫(xiě)之后插入StoreLoad屏障將帶來(lái)可觀的執(zhí)行效率的提升殴俱。從這里也可看出JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性政冻,然后再去追求效率(其實(shí)我們工作中編碼也是一樣)枚抵。
下面是在保守策略下,volatile讀插入內(nèi)存屏障后生產(chǎn)的指令序列示意圖:
上述volatile寫(xiě)和volatile讀的內(nèi)存屏障插入策略非常保守明场。在實(shí)際執(zhí)行時(shí)汽摹,只要不改變volatile寫(xiě)-讀的內(nèi)存語(yǔ)義,編譯器可以根據(jù)具體情況忽略不必要的屏障苦锨。在JMM基礎(chǔ)中就有提到過(guò)各個(gè)處理器對(duì)各個(gè)屏障的支持度逼泣,其中x86處理器僅會(huì)對(duì)寫(xiě)-讀操作做重排序。
原子性
原子性:即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷舟舒,要么就都不執(zhí)行拉庶。
一個(gè)很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問(wèn)題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個(gè)操作:從賬戶A減去1000元秃励,往賬戶B加上1000元砍的。
試想一下,如果這2個(gè)操作不具備原子性莺治,會(huì)造成什么樣的后果。假如從賬戶A減去1000元之后帚稠,操作突然中止谣旁。然后又從B取出了500元,取出500元之后滋早,再執(zhí)行 往賬戶B加上1000元 的操作榄审。這樣就會(huì)導(dǎo)致賬戶A雖然減去了1000元,但是賬戶B沒(méi)有收到這個(gè)轉(zhuǎn)過(guò)來(lái)的1000元杆麸。
所以這2個(gè)操作必須要具備原子性才能保證不出現(xiàn)一些意外的問(wèn)題搁进。
同樣地反映到并發(fā)編程中會(huì)出現(xiàn)什么結(jié)果呢?
舉個(gè)最簡(jiǎn)單的例子昔头,大家想一下假如為一個(gè)32位的變量賦值過(guò)程不具備原子性的話饼问,會(huì)發(fā)生什么后果?
i = 9;
假若一個(gè)線程執(zhí)行到這個(gè)語(yǔ)句時(shí)揭斧,我暫且假設(shè)為一個(gè)32位的變量賦值包括兩個(gè)過(guò)程:為低16位賦值莱革,為高16位賦值。
那么就可能發(fā)生一種情況:當(dāng)將低16位數(shù)值寫(xiě)入之后讹开,突然被中斷盅视,而此時(shí)又有一個(gè)線程去讀取i的值,那么讀取到的就是錯(cuò)誤的數(shù)據(jù)旦万。
對(duì)于被 volatile 修飾的變量闹击,對(duì)任意(包括64位long類(lèi)型和double類(lèi)型)單個(gè)volatile變量的讀/寫(xiě)具有原子性,記著是對(duì)單個(gè)volatile變量的讀或?qū)懖啪哂性有猿伤遥硗馊魏螐?fù)合操作都不能保證原子性赏半,如a++贺归,a = a+1, a = b。特別注意a = b這類(lèi)除破,它實(shí)際上包含2個(gè)操作牧氮,它先要去讀取b的值,再將b的值寫(xiě)入工作內(nèi)存瑰枫,雖然讀取b的值以及將b的值寫(xiě)入工作內(nèi)存這2個(gè)操作都是原子性操作踱葛,但是合起來(lái)就不是原子性操作了。
想要理解透volatile特性有一個(gè)很好的方法光坝,就是把對(duì)volatile變量的單個(gè)讀/寫(xiě)尸诽,看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫(xiě)操作做了同步。
總結(jié)
至此應(yīng)該對(duì) volatile 有比較好的了解了盯另,至少面試應(yīng)該問(wèn)題不大了性含,其實(shí)就是上面幾個(gè)關(guān)鍵點(diǎn),可見(jiàn)性鸳惯,重排序商蕴,內(nèi)存屏障,原子性芝发。把這些底層都研究透了绪商,面試官根本難不倒你。
參考資料:
深入理解 Java 虛擬機(jī)
Java 并發(fā)編程的藝術(shù)
Java 多線程編程核心技術(shù)
https://www.cnblogs.com/yuanfy008/p/9335168.html
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/chenssy/p/6379280.html