? ? ? ? 上篇文章介紹了Java內(nèi)存模型抚芦,沒(méi)看過(guò)《深入理解Java虛擬機(jī)》的同學(xué)可以去看下Java內(nèi)存模型
? ? ? ? Java內(nèi)存模型對(duì)volatile專門(mén)定義了一些特殊的訪問(wèn)規(guī)則,假定T表示一個(gè)線程袖订,V和W分別表示兩個(gè)volatile變量勤揩,那么在進(jìn)行read旋奢、load润匙、use疫诽、assign、store鳍鸵、和write操作時(shí)需要滿足如下規(guī)則
1)只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作時(shí)load的時(shí)候苇瓣,線程T才能對(duì)變量V執(zhí)行use動(dòng)作。并且只有線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候权纤,線程T才能對(duì)變量V執(zhí)行l(wèi)oad動(dòng)作钓简。線程T對(duì)變量V的use動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的load、read動(dòng)作相關(guān)聯(lián)汹想,必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中外邓,每次使用V前都必須先從主內(nèi)存刷新最新的值,用于保證能看見(jiàn)其他線程對(duì)變量V所做的修改后的值)
2)只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作時(shí)assign的時(shí)候古掏,線程T才能對(duì)變量V執(zhí)行store動(dòng)作损话,并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store的時(shí)候槽唾,線程T才能對(duì)變量V執(zhí)行assign動(dòng)作丧枪。線程T對(duì)變量V的assign動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的store、wtite動(dòng)作相關(guān)聯(lián)庞萍,必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中拧烦,每次修改V后都必須立刻同步回主內(nèi)存中,用于保證其他線程可以看見(jiàn)自己對(duì)變量V所做的修改)
3)假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作钝计,動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作恋博,假定動(dòng)作P是和動(dòng)作F相對(duì)應(yīng)的對(duì)變量V的read或write動(dòng)作;類似的私恬,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作债沮,假定動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)變量W的read或write動(dòng)作本鸣。如果A先于B疫衩,那么P先于Q(這條規(guī)則要求volatile修改的變量不會(huì)被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同)
可以說(shuō)volatile關(guān)鍵字是Java虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制荣德。
? ? ? ? 當(dāng)一個(gè)變量定義為volatile之后具備如下特性:
? ? ? ? 1.可見(jiàn)性:指當(dāng)一條線程修改了這個(gè)變量的值闷煤,新值對(duì)于其他線程來(lái)說(shuō)是可以立即得知的。普通變量的值在線程間傳遞均需通過(guò)主內(nèi)存來(lái)完成涮瞻,例:線程A修改了一個(gè)普通變量的值曹傀,然后向主內(nèi)存進(jìn)行回寫(xiě),另外一條線程B在線程A回寫(xiě)完成了后再?gòu)闹鲀?nèi)存進(jìn)行讀取操作饲宛,新變量的值才會(huì)對(duì)線程B可見(jiàn)。
? ? ? ? 雖然volatile型變量是線程可見(jiàn)的嗜价,對(duì)volatile變量所有的讀寫(xiě)都能立刻反應(yīng)到其他線程之中艇抠,但基于volatile變量的運(yùn)算在并發(fā)下并不是安全的幕庐,因?yàn)镴ava里面的運(yùn)算并不是原子操作。例:race ++家淤,我們用javap反編譯這句代碼會(huì)發(fā)現(xiàn)在Class文件中是由4條字節(jié)碼指令構(gòu)成异剥,(這里圖1是書(shū)中原圖,圖2是筆者自己反編譯的絮重,可能由于Java虛擬機(jī)版本的問(wèn)題2者不太一樣冤寿,請(qǐng)知道的大佬告知,為文章嚴(yán)謹(jǐn)性看圖1就可以了)
從字節(jié)碼層面很容易分析出來(lái)青伤,假如有兩條線程同時(shí)執(zhí)行這條語(yǔ)句督怜,線程A執(zhí)行g(shù)etstatic指令把race的值取到操作棧頂時(shí),volatile關(guān)鍵字保證來(lái)race的值在此時(shí)是正確的狠角,但是執(zhí)行iconst_1号杠、iadd這些指令時(shí)線程B可能已經(jīng)把race的值加大了,而在操作棧定的值就變成了過(guò)期的數(shù)據(jù)丰歌。
? ? ? ? 由于volatile變量只能保證可見(jiàn)性姨蟋,所以在不符合以下兩條規(guī)則的運(yùn)算場(chǎng)景中,我們?nèi)匀灰ㄟ^(guò)加鎖來(lái)保證原子性
? ? ? ? 1)運(yùn)算結(jié)果并不依賴變量的當(dāng)前值立帖,或者能夠確保只有單一的線程修改變量的值
? ? ? ? 2)變量不需要與其他的狀態(tài)變量共同參與不變約束
2.禁止指令重排序優(yōu)化眼溶,這里說(shuō)明下,為了使處理器內(nèi)部的運(yùn)算單元能盡量被充分利用晓勇,處理器可能會(huì)對(duì)輸入代碼進(jìn)行亂序執(zhí)行優(yōu)化堂飞。與之類似的Java虛擬機(jī)的即時(shí)編譯器也有指令重排序優(yōu)化。還不知道的同學(xué)可以去查下(說(shuō)的就是我自己)宵蕉。
? ? ? ? 普通的變量?jī)H僅會(huì)保證在該方法的執(zhí)行過(guò)程中所有依賴值結(jié)果的地方都能獲取到正確的結(jié)果酝静,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。這樣理解起來(lái)還是有點(diǎn)抽象羡玛,筆者舉個(gè)自己在書(shū)中看了茅塞頓開(kāi)的例子
這里是一段匯編代碼别智,方便起見(jiàn)就用書(shū)中原圖了
這是段標(biāo)準(zhǔn)的DCL單例,變量用valatile修飾稼稿,賦值后(mov%eax薄榛,0x150(%esi)這句便是賦值操作)多執(zhí)行了一個(gè)“l(fā)ock addl & 0x0,(%esp)”操作让歼,這個(gè)操作相當(dāng)于一個(gè)內(nèi)存屏障敞恋,只有一個(gè)cpu訪問(wèn)時(shí)并不需要內(nèi)存屏障,但如果有兩個(gè)或更多cpu訪問(wèn)同一塊內(nèi)存谋右,且其中一個(gè)在觀測(cè)另一個(gè)硬猫,就需要內(nèi)存屏障來(lái)保證一致性了。這句指令“addl & 0x0,(%esp)”(把esp寄存器的值加0)顯然是一個(gè)空操作啸蜜,關(guān)鍵在于lock前綴坑雅,它的作用是使得本cpu的Cache寫(xiě)入了內(nèi)存,該寫(xiě)入動(dòng)作也會(huì)引起別的cpu或者別的內(nèi)核無(wú)效化其Cache衬横,這中操作箱單于對(duì)Cache中的變量做了一個(gè)“store和write”操作裹粤。所以通過(guò)這樣一個(gè)空操作可以讓前面valatile變量的修改對(duì)其他cpu立即可見(jiàn)。
? ? ? ? 這段基本是書(shū)中原文蜂林,這樣說(shuō)可能有的同學(xué)不太懂(我就沒(méi)懂)遥诉,下面在結(jié)合具體代碼說(shuō)下,instance = new Singleton() 這句代碼并不是一個(gè)原子操作噪叙,大致做了3件事情
1)給Singleton實(shí)例分配內(nèi)存空間
2)初始化Singleton對(duì)象
3)將instance對(duì)象指向分配的內(nèi)存空間
由于Java虛擬機(jī)指令重排序優(yōu)化矮锈,使得2、3的順序是無(wú)法保證的构眯,如果出現(xiàn)了132的情況愕难,并且在3執(zhí)行完畢2未執(zhí)行之前線程切換了,這個(gè)時(shí)候instance非空惫霸,所以直接返回instance猫缭,結(jié)果可想而知
? ? 避免篇幅過(guò)長(zhǎng)就到這吧