了解volatile語(yǔ)義對(duì)了解多線程的其他特性很有意義,所以把它放在前面討論。
volatile是JVM提供的最輕量級(jí)的同步機(jī)制。volatile提供單個(gè)field的內(nèi)存同步控制,synchronized則提供整個(gè)臨界區(qū)(代碼塊/方法)的同步控制。
2個(gè)基本特性
當(dāng)一個(gè)變量定義為volatile之后,它將具備兩種特性,第一是確保了此變量對(duì)所有線程的(實(shí)時(shí))可見(jiàn)性1。普通變量做不到這一點(diǎn),普通變量的值在線程間傳遞是異步的,需要通過(guò)主內(nèi)存來(lái)完成,例如,線程A修改一個(gè)普通變量值,然后向主內(nèi)存進(jìn)行回寫,另外一條線程B在線程A回寫完成之后再?gòu)闹鲀?nèi)存進(jìn)行讀取操作,新變量值才會(huì)對(duì)線程B可見(jiàn)。雖然volatile變量在各個(gè)線程的工作內(nèi)存中不存在一致性問(wèn)題,但是Java里的運(yùn)算并非全是原子操作,例如復(fù)合操作int++,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。
《深入理解Java虛擬機(jī)》p366?中舉了一個(gè)例子:代碼發(fā)起20個(gè)新線程,每個(gè)線程對(duì)volatile變量(race,初始值=0)進(jìn)行10000次自增,如果代碼能正確并發(fā)的話,正確結(jié)果應(yīng)該是200000,但實(shí)際并非如此,誤差極大(15W+ ~ 18W+)。問(wèn)題出在race++自增運(yùn)算中。當(dāng)指令把race值取到操作棧頂時(shí),volatile關(guān)鍵字保證了race的值在此時(shí)是正確的,但是在執(zhí)行iconst_1,iadd這些指令時(shí),其他線程可能已經(jīng)把race的值加大了,而在操作棧頂?shù)闹稻妥兂闪诉^(guò)期的數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的race值同步回主內(nèi)存之中。即使編譯出來(lái)的只有一條字節(jié)碼指令,也不意味著它是一個(gè)原子操作。一條字節(jié)碼指令也可能轉(zhuǎn)化成若干條本地機(jī)器碼指令。
由于volatile變量只能保證可見(jiàn)性,在不符合以下兩條規(guī)則的運(yùn)算場(chǎng)景中,仍然要通過(guò)加鎖來(lái)保證原子性,如synchronized或juc原子類。
? ? 1、運(yùn)算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一線程修改變量的值。
? ? 2、變量不需要與其他的狀態(tài)變量共同參與不變約束。
volatile的第二個(gè)語(yǔ)義是禁止指令重排序優(yōu)化。普通變量?jī)H僅會(huì)保證在該方法的執(zhí)行過(guò)程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。就是說(shuō)JVM會(huì)保證有賦值依賴關(guān)系的操作順序,但如果只有業(yè)務(wù)依賴關(guān)系的話,JVM是無(wú)法識(shí)別,也就無(wú)法保證了。
《深》p369中舉的例子,線程A讀取配置文件,然后將initialized置為true,線程B會(huì)一直檢查initialized,如為true則開(kāi)始其他操作。這個(gè)邏輯關(guān)系JVM是無(wú)法感知的,因?yàn)樽x取配置文件與initialized賦值之間并無(wú)賦值依賴關(guān)系。如果initialized變量沒(méi)有使用volatile修飾,就可能由于指令重排序的優(yōu)化,導(dǎo)致線程A最后一句initialized=true對(duì)應(yīng)的指令被提前執(zhí)行,這樣線程線程B中使用配置信息的代碼就可能出現(xiàn)錯(cuò)誤,而volatile關(guān)鍵字則可以避免此類情況。
對(duì)于一段DCL實(shí)現(xiàn)單例的代碼,通過(guò)對(duì)比加入volatile和未加入volatile時(shí)的匯編代碼的差別,發(fā)現(xiàn)關(guān)鍵變化在于有volatile修飾的變量,賦值后多了一個(gè)“l(fā)ock addl $0x0, (%esp)”操作,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障(指重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置)。“l(fā)ock addl”指令的作用是使得本CPU的Cache寫入內(nèi)存,該寫入動(dòng)作也會(huì)引起其他CPU無(wú)效化其Cache,相當(dāng)于對(duì)Cache變量做了一次store+write操作,讓前面volatile變量的操作對(duì)其他CPU立即可見(jiàn)。
JMM對(duì)volatile變量定義的特殊規(guī)則
假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile變量,那么在進(jìn)行read、load、use、assign、store、write操作時(shí)需要滿足以下規(guī)則:
1、load + use 必須成對(duì)出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須先從主內(nèi)存中刷新最新的值,用于保證能看見(jiàn)其他線程對(duì)變量V所做的修改后的值。
2、assign + store 必須成對(duì)出現(xiàn)。這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看到自己對(duì)變量V的修改。
3、這條規(guī)則要求volatile修飾的變量不會(huì)被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同。
假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use/assign,相同,動(dòng)作B是線程T對(duì)變量W實(shí)施的use/assign動(dòng)作;
假定動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load/store動(dòng)作,相同,動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load/store動(dòng)作;
假定動(dòng)作P是和動(dòng)作F相應(yīng)的對(duì)變量V的read/write動(dòng)作,相同,動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)變量W的read/write動(dòng)作;
如果A先于B,那么P先于Q。
注1:JVM對(duì)“實(shí)時(shí)可見(jiàn)性”的實(shí)現(xiàn),并非是絕對(duì)的。在各個(gè)線程的工作內(nèi)存中,volatile變量可以存在不一致,但每次使用前都要先刷新,執(zhí)行引擎看不到不一致的情況,因此可以認(rèn)為不存在一致性問(wèn)題。
注2:volatile屏蔽指令重排序的語(yǔ)義在JDK1.5中才被完全修復(fù),此前的JDK無(wú)法保證volatile變量完成避免重排序?qū)е碌膯?wèn)題,這點(diǎn)也是在JDK1.5之前無(wú)法安全使用DCL(雙鎖檢測(cè))來(lái)實(shí)現(xiàn)單例模式的原因。
參考資料
? ? ?《深入理解Java虛擬機(jī)》第二版 12.3.3小節(jié)