在Java并發(fā)編程中,volatile和synchronized都扮演者重要的角色。volatile又被成為輕量級(jí)的synchronized,它保證了共享變量的可見性饿自。
注:何謂可見性汰翠?
通俗點(diǎn)兒說,可見性就是當(dāng)一個(gè)線程修改一個(gè)共享變量時(shí)昭雌,其他的線程也可以讀到修改后的值复唤。
如果volatile使用得當(dāng),由于不會(huì)引起線程的上下文切換和調(diào)度城豁,它的開銷會(huì)比synchronized關(guān)鍵字小很多苟穆。本文將會(huì)深入分析volatile關(guān)鍵字實(shí)現(xiàn)原理,相信通過本文唱星,大家可以更好的使用volatile關(guān)鍵字雳旅。
如何保證可見性
在Java內(nèi)存模型(Java Memory Model,JMM)中间聊,線程和主存之間存在這樣的抽象關(guān)系:
線程之間的共享變量存放在主內(nèi)存(Main Memory)中攒盈;
每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),存放了當(dāng)前線程共享變量的副本哎榴。
那針對(duì)被volatile修飾的變量的讀/寫到底是什么樣子的呢型豁?
當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把當(dāng)前線程的本地內(nèi)存中的共享變量值刷新到主內(nèi)存中尚蝌;
當(dāng)讀一個(gè)volatile變量時(shí)迎变,JMM會(huì)把當(dāng)前線程的本地內(nèi)存置為無效,然后從主內(nèi)存中讀取共享變量飘言。
對(duì)Java內(nèi)存模型有一些了解的一定對(duì)指令重排序不陌生衣形,一個(gè)程序在運(yùn)行中,編譯器和處理器為了優(yōu)化程序性能姿鸿,會(huì)對(duì)指令序列進(jìn)行重排序谆吴,那這時(shí)候就有問題了,那既然指令會(huì)被重新排序苛预,volatile是怎么保證內(nèi)存可見性呢句狼?萬一被重排了,內(nèi)存可見不就會(huì)有問題了~當(dāng)然咯热某,為了保證volatile的內(nèi)存可見性腻菇,編譯器在生成字節(jié)碼的時(shí)候會(huì)在指令序列中插入內(nèi)存屏障(Memory Barriers)來禁止特定類型的編譯器和處理器重排序。一旦內(nèi)存屏障被插入昔馋,就相當(dāng)于告訴了編譯器和處理器:不管是什么指令都不能和內(nèi)存屏障進(jìn)行重排序芜繁。
注:內(nèi)存屏障(Memory Barriers):一組CPU指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制绒极。
我們還是以一個(gè)簡(jiǎn)單的例子來解釋下:
從test()方法可以看出:
- 如果d不是volatile變量,1/2/3三個(gè)語句是可以進(jìn)行隨意進(jìn)行指令重排序的蔬捷;
注:為什么4不能和1/2/3一起指令重排序垄提?因?yàn)?和1/2/3存在數(shù)據(jù)依賴關(guān)系榔袋,編譯器和處理器在重排序時(shí)會(huì)遵守?cái)?shù)據(jù)依賴性,不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序铡俐。所以4不會(huì)參與到1/2/3的重排序行列凰兑。
- d被volatile修飾后,會(huì)在1/2后插入一個(gè)內(nèi)存屏障审丘,此時(shí)吏够,1/2可以隨意進(jìn)行指令重排序,3不再參與到重排序行列滩报。
接下來我們?cè)趚86下通過工具生成的匯編指令來看看對(duì)volatile變量進(jìn)行讀寫操作時(shí)CPU會(huì)干什么锅知。
instance = new Singleton(); //instance是volatile變量
匯編指令:
lock addl $0x0, (%rsp)
從匯編指令可以看出,volatile變量在寫操作之前使用了lock前綴脓钾,lock前綴指令在多處理器下會(huì)引發(fā)兩件事情:
將當(dāng)前處理器的緩存行里的數(shù)據(jù)寫回到內(nèi)存售睹。lock前綴指令的在執(zhí)行期間會(huì)產(chǎn)生LOCK#信號(hào),該信號(hào)會(huì)鎖住總線可训,導(dǎo)致其他CPU不能訪問總線昌妹。由于鎖總線開銷比較大,在最近的處理器中握截,LOCK#信號(hào)不鎖總線了飞崖,而是鎖緩存并回寫到主存,用緩存一直性機(jī)制來確保修改的原子性(該操作又被稱之為緩存鎖定)谨胞;
寫回內(nèi)存的操作會(huì)使其他CPU的緩存無效固歪。IA-32處理器和Intel 64處理器使用MESI(修改,獨(dú)占畜眨,共享昼牛,無效)控制協(xié)議去維護(hù)內(nèi)部緩存和其他處理器緩存的一致性。IA-32和Intel 64處理器使用嗅探技術(shù)保證各處理器緩存和系統(tǒng)內(nèi)存的數(shù)據(jù)在總線上保持一致康聂。如果一個(gè)處理器通過嗅探檢測(cè)到其他處理器準(zhǔn)備寫內(nèi)存贰健,并且該內(nèi)存地址處于共享狀態(tài),該處理器會(huì)將它的緩存行置為無效恬汁,下次再操作該內(nèi)存地址時(shí)會(huì)重新把數(shù)據(jù)讀到緩存行中伶椿。
使用場(chǎng)景
多嘴兩句,volatile雖然在某些情況下性能要優(yōu)于synchronized氓侧,但是由于volatile無法保證操作的原子性脊另,所以volatile是無法替代synchronized的。
通常來說约巷,使用volatile必須具備以下2個(gè)條件:
對(duì)變量的寫操作不依賴于當(dāng)前值偎痛,比如++操作,volatile不能保證多線程情況下操作的正確性的独郎;
該變量沒有包含在具有其他變量的不變式中踩麦。
下面列舉兩個(gè)我們平常在工作中使用較多的場(chǎng)景:
- 狀態(tài)標(biāo)記
需求描述:根據(jù)狀態(tài)標(biāo)記進(jìn)行消息發(fā)送枚赡;
代碼示例:
public class MessageProcessor {
private volatile boolean flag;
public void process() {
if (flag) {
sendMessage();
} else {
recordLog();
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
用volatile修飾flag,如果有其他線程修改過該標(biāo)識(shí)谓谦,其他線程都可以拿到最新值贫橙,根據(jù)最新值各個(gè)線程可以進(jìn)行相關(guān)的操作。
-
單例模式的double check
很多人在使用double check的時(shí)候都不用volatile修飾反粥,程序也能正常運(yùn)行卢肃,但是其實(shí)不使用volatile修飾是有問題的。那會(huì)有什么問題呢才顿?
其實(shí)會(huì)出問題的主要是instance = new Singleton();
莫湘,它并不是一個(gè)原子操作,在JVM中娜膘,這行代碼主要完成了這樣3件事:a. 為對(duì)象分配內(nèi)存空間逊脯;
b. 初始化對(duì)象;
c. 將instance對(duì)象指向分配的內(nèi)存空間,執(zhí)行完這一步驟竣贪,instance就不為null了军洼。
在指令重排序時(shí),a-b-c的順序可能會(huì)被重排成a-c-b演怎,如果現(xiàn)在執(zhí)行順序被重排成a-c-b匕争,在單線程情況下不會(huì)影響程序執(zhí)行的結(jié)果,但是在多線程情況下就不一樣了爷耀,如果線程A執(zhí)行了指令c甘桑,此時(shí)instance實(shí)例還沒有被初始化好,但是已經(jīng)不為null了歹叮,剛好線程B執(zhí)行到
if (null == instance)
跑杭,發(fā)現(xiàn)instance不為空,隨即返回咆耿,但是得到的卻是未被完全初始化的實(shí)例德谅,在使用的時(shí)候就會(huì)有問題。