26張圖帶你徹底搞懂volatile關(guān)鍵字
引子
小艾吃飯路上碰上小牛,忙問:你昨天面大廠面的咋樣了?聽說他們最喜歡問多線程相關(guān)知識(shí)。
小牛說:對(duì)啊,第一個(gè)問題我就講了20分鐘,直接把面試官講服了。
小艾忙問:什么問題能講這么久?是不是問你情感經(jīng)歷了?
小牛說:...問的volatile關(guān)鍵字。
小艾說:volatile關(guān)鍵詞的作用一般有如下兩個(gè):
- 可見性:當(dāng)一個(gè)線程修改了由volatile關(guān)鍵字修飾的變量的值時(shí),其它線程能夠立即得知這個(gè)修改。
- 有序性:禁止編譯器關(guān)于操作volatile關(guān)鍵詞修飾的變量的指令重排序。
你說這兩個(gè)說了20分鐘?口吃?
小牛說:你知道volatile的實(shí)現(xiàn)原理嗎?
小艾說:緩存一致性協(xié)議嘛,這有啥?
小牛說:既然硬件保證了緩存一致性協(xié)議,無(wú)論該變量是否被volatile關(guān)鍵詞修飾,它都該滿足緩存一致性協(xié)議呀。你這說的有點(diǎn)自相矛盾哦。
小艾說:那volatile的實(shí)現(xiàn)原理是什么?
小牛說:且聽我慢慢道來(lái)。
緩存一致性協(xié)議
我們知道,現(xiàn)代CPU都是多核處理器。由于cpu核心(Kernel)讀取內(nèi)存數(shù)據(jù)較慢,于是就有了緩存的概念。我們希望針對(duì)頻繁讀寫的某個(gè)內(nèi)存變量,提升本核心的訪問速率。因此我們會(huì)給每個(gè)核心設(shè)計(jì)緩存區(qū)(Cache),緩存該變量。由于緩存硬件的讀寫速度比內(nèi)存快,所以通過這種方式可以提升變量訪問速度。緩存的結(jié)構(gòu)可以如下設(shè)計(jì):
緩存結(jié)構(gòu)圖
其中,一個(gè)緩存區(qū)可以分為N個(gè)緩存行(Cache line),緩存行是和內(nèi)存進(jìn)行數(shù)據(jù)交換的最小單位。每個(gè)緩存行包含三個(gè)部分,其中valid用于標(biāo)識(shí)該數(shù)據(jù)的有效性。如果有效位為false,CPU核心就從內(nèi)存中讀取,并將對(duì)應(yīng)舊的緩存行數(shù)據(jù)覆蓋,否則使用舊緩存數(shù)據(jù);tag用于指示數(shù)據(jù)對(duì)應(yīng)的內(nèi)存地址;block則用以存儲(chǔ)數(shù)據(jù),
多核緩存和內(nèi)存
但是,如果涉及到并發(fā)任務(wù),多個(gè)核心讀取同一個(gè)變量值,由于每個(gè)核心讀取的是自己那一部分的緩存,每個(gè)核心的緩存數(shù)據(jù)不一致將會(huì)導(dǎo)致一系列問題。緩存一致性的問題根源就在于,對(duì)于某個(gè)變量,好幾個(gè)核心對(duì)應(yīng)的緩存區(qū)都有,到底哪個(gè)是新的數(shù)據(jù)呢?如果只有一個(gè)CPU核心對(duì)應(yīng)的緩存區(qū)有該變量,那就沒事啦,該緩存肯定是新的。
所以為了保證緩存的一致性,業(yè)界有兩種思路:
- 寫失效(Write Invalidate):當(dāng)一個(gè)核心修改了一份數(shù)據(jù),其它核心如果有這份數(shù)據(jù),就把valid標(biāo)識(shí)為無(wú)效;
- 寫更新(Write update):當(dāng)一個(gè)核心修改了一份數(shù)據(jù),其它核心如果有這份數(shù)據(jù),就都更新為新值,并且還是標(biāo)記valid有效。
業(yè)界有多種實(shí)現(xiàn)緩存一致性的協(xié)議,諸如MSI、MESI、MOSI、Synapse、Firefly Dragon Protocol等,其中最為流行的是MESI協(xié)議。
MESI協(xié)議就是根據(jù)寫失效的思路,設(shè)計(jì)的一種緩存一致性協(xié)議。為了實(shí)現(xiàn)這個(gè)協(xié)議,原先的緩存行修改如下:
緩存結(jié)構(gòu)圖
原先的valid是一個(gè)比特位,代表有效/無(wú)效兩種狀態(tài)。在MESI協(xié)議中,該位改成兩位,不再只是有效和無(wú)效兩種狀態(tài),而是有四個(gè)狀態(tài),分別為:
- M(Modified):表示核心的數(shù)據(jù)被修改了,緩存數(shù)據(jù)屬于有效狀態(tài),但是數(shù)據(jù)只處于本核心對(duì)應(yīng)的緩存,還沒有將這個(gè)新數(shù)據(jù)寫到內(nèi)存中。由于此時(shí)數(shù)據(jù)在各個(gè)核心緩存區(qū)只有唯一一份,不涉及緩存一致性問題;
- E(Exclusive):表示數(shù)據(jù)只存在本核心對(duì)應(yīng)的緩存中,別的核心緩存沒這個(gè)數(shù)據(jù),緩存數(shù)據(jù)屬于有效狀態(tài),并且該緩存中的最新數(shù)據(jù)已經(jīng)寫到內(nèi)存中了。同樣由于此時(shí)數(shù)據(jù)在各個(gè)核心緩存區(qū)只有一份,也不涉及緩存一致性問題;
- S(Shared):表示數(shù)據(jù)存于多個(gè)核心對(duì)應(yīng)的緩存中,緩存數(shù)據(jù)屬于有效狀態(tài),和內(nèi)存一致。這種狀態(tài)的值涉及緩存一致性問題;
- I(Invalid):表示該核心對(duì)應(yīng)的緩存數(shù)據(jù)無(wú)效。
看到這里,大家想必知道為什么這個(gè)協(xié)議稱為MESI協(xié)議了吧,它的命名就是取了這四個(gè)狀態(tài)的首字母而已。為了保證緩存一致性,每個(gè)核心要寫新數(shù)據(jù)前,需要確保其他核心已經(jīng)置同一變量數(shù)據(jù)的緩存行狀態(tài)位為Invalid后,再把新數(shù)據(jù)寫到自己的緩存行,并之后寫到內(nèi)存中。
MESI協(xié)議包含以下幾個(gè)行為:
- 讀(Read):當(dāng)某個(gè)核心需要某個(gè)變量的值,并且該核心對(duì)應(yīng)的緩存沒這個(gè)變量時(shí),就會(huì)發(fā)出讀命令,希望別的核心緩存或者內(nèi)存能給該核心最新的數(shù)據(jù);
- 讀命令反饋(Read Response):讀命令反饋是對(duì)讀命令的回應(yīng),包含了之前讀命令請(qǐng)求的數(shù)據(jù)。舉例來(lái)說,Kernel0發(fā)送讀命令,請(qǐng)求變量a的值,Kernel1對(duì)應(yīng)的緩存區(qū)包含變量a,并且該緩存的狀態(tài)是M狀態(tài),所以Kernel1會(huì)給Kernel0的讀命令發(fā)送讀命令反饋,給出該值;
- 無(wú)效化(Invalidate):無(wú)效化指令是一條廣播指令,它告訴其他所有核心,緩存中某個(gè)變量已經(jīng)無(wú)效了。如果變量是獨(dú)占的,只存在某一個(gè)核心對(duì)應(yīng)的緩存區(qū)中,那就不存在緩存一致性問題了,直接在自己緩存中改了就行,也不用發(fā)送無(wú)效化指令;
- 無(wú)效化確認(rèn)(Invalidate Acknowledge):該指令是對(duì)無(wú)效化指令的回復(fù),收到無(wú)效化指令的核心,需要將自己緩存區(qū)對(duì)應(yīng)的變量狀態(tài)改為Invalid,并回復(fù)無(wú)效化確認(rèn),以此保證發(fā)送無(wú)效化確認(rèn)的緩存已經(jīng)無(wú)效了;
- 讀無(wú)效(Read Invalidate):這個(gè)命令是讀命令和無(wú)效化命令的綜合體。它需要接受讀命令反饋和無(wú)效化確認(rèn);
- 寫回(Writeback)這個(gè)命令的意思是將核心中某個(gè)緩存行對(duì)應(yīng)的變量值寫回到內(nèi)存中去。
下圖給了個(gè)一個(gè)應(yīng)用MESI讀寫數(shù)據(jù)的例子。在該圖中,假設(shè)CPU有兩個(gè)核心,Kernel0表示第一個(gè)核心,Kernel1表示第二個(gè)核心。這里給出了Kernel0想寫新數(shù)據(jù)到自己緩存的例子。
- 首先Kernel0先完成新數(shù)據(jù)的創(chuàng)建;
- Kernel0向全體其他核心發(fā)送無(wú)效化指令,告訴其他核心其所對(duì)應(yīng)的緩存區(qū)中的這條數(shù)據(jù)已經(jīng)過期無(wú)效。本圖例中只有一個(gè)其他核心,為Kernel1;
- 其他核心收到廣播消息后,將自己對(duì)應(yīng)緩存的數(shù)據(jù)的標(biāo)志位記為無(wú)效,然后給Kernel0回確認(rèn)消息;
- 收到所有其他Kernel的確認(rèn)消息后,Kernel0才能將新數(shù)據(jù)寫回到它所對(duì)應(yīng)的緩存結(jié)構(gòu)中去。
根據(jù)上圖,我們可以發(fā)現(xiàn),影響MESI協(xié)議的時(shí)間瓶頸主要有兩塊:
- 無(wú)效化指令:Kernel0需要通知所有的核心,該變量對(duì)應(yīng)的緩存在其他核心中是無(wú)效的。在通知完之前,該核心不能做任何關(guān)于這個(gè)變量的操作。
- 確認(rèn)響應(yīng):Kernel0需要收到其他核心的確認(rèn)響應(yīng)。在收到確認(rèn)消息之前,該核心不能做任何關(guān)于這個(gè)變量的操作,需要持續(xù)等待其他核心的響應(yīng),直到所有核心響應(yīng)完成,將其對(duì)應(yīng)的緩存行標(biāo)志位設(shè)為Invalid,才能繼續(xù)其它操作。
針對(duì)這兩部分,我們可以進(jìn)一步優(yōu)化:
- 針對(duì)無(wú)效化指令的加速:在緩存的基礎(chǔ)上,引入Store Buffer這個(gè)結(jié)構(gòu)。Store Buffer是一個(gè)特殊的硬件存儲(chǔ)結(jié)構(gòu)。通俗的來(lái)講,核心可以先將變量寫入Store Buffer,然后再處理其他事情。如果后面的操作需要用到這個(gè)變量,就可以從Store Buffer中讀取變量的值,核心讀數(shù)據(jù)的順序變成Store Buffer → 緩存 → 內(nèi)存。這樣在任何時(shí)候核心都不用卡住,做不了關(guān)于這個(gè)變量的操作了。引入Store Buffer后的結(jié)構(gòu)如下所示:
Store Buffer結(jié)構(gòu)
- 針對(duì)確認(rèn)響應(yīng)的加速:在緩存的基礎(chǔ)上,引入Invalidate Queue這個(gè)結(jié)構(gòu)。其他核心收到Kernel0的Invalidate的命令后,立即給Kernel0回Acknowledge,并把Invalidate這個(gè)操作,先記錄到Invalidate Queue里,當(dāng)其他操作結(jié)束時(shí),再?gòu)腎nvalidate Queue中取命令,進(jìn)行Invalidate操作。所以當(dāng)Kernel0收到確認(rèn)響應(yīng)時(shí),其他核心對(duì)應(yīng)的緩存行可能還沒完全置為Invalid狀態(tài)。引入Invalidate Queue后的結(jié)構(gòu)如下所示:
Invalidate Queue結(jié)構(gòu)
緩存一致性協(xié)議優(yōu)化存在的問題
上一節(jié)講了兩種緩存一致性協(xié)議的加速方式。但是這兩個(gè)方式卻會(huì)對(duì)緩存一致性導(dǎo)致一定的偏差,下面我們來(lái)看一下兩個(gè)出錯(cuò)的例子:
例子1:關(guān)于Store Buffer帶來(lái)的錯(cuò)誤,假設(shè)CPU有兩個(gè)核心,Kernel0表示第一個(gè)核心,Kernel1表示第二個(gè)核心。
...
public void foo(){
a=1;
b=1;
}
public void bar(){
while(b==0) continue;
assert(a==1):"a has a wrong value!";
}
...
如果Kernel0執(zhí)行foo()函數(shù),Kernel1執(zhí)行bar()函數(shù),按照之前我們的理解,如果b變量為1了,那a肯定為1了,assert(a==1)肯定不會(huì)報(bào)錯(cuò)。但是事實(shí)卻不是這樣的。
假設(shè)初始情況是這樣的:在執(zhí)行兩個(gè)函數(shù)前Kernel1的緩存包含變量a=0,不包含緩存變量b,Kernel0的緩存包含變量b=0,不包含緩存變量a。Kernel0執(zhí)行foo()函數(shù),Kernel1執(zhí)行bar()函數(shù)時(shí),。這樣的話計(jì)算機(jī)的指令程序可能會(huì)如下展開:
- Kernel0執(zhí)行a=1。由于Kernel0的緩存行不包含變量a,因此Kernel0會(huì)將變量a的值存在Store Buffer中,并且向其他Kernel進(jìn)行read Invalidate操作,通知a變量緩存無(wú)效;
- Kernel1執(zhí)行while(b==0),由于Kernel1的緩存沒有變量b,因此它需要發(fā)送一個(gè)讀命令,去找b的值;
- Kernel0執(zhí)行b=1,由于Kernel0的緩存中已經(jīng)有了變量b,而且別的核心沒有這個(gè)變量的緩存,所以它可以直接更改緩存b的值;
- Kernel0收到讀命令后,將最新的b的值發(fā)送給Kernel1,并且將變量b的狀態(tài)由E(獨(dú)占)改變?yōu)镾(共享);
- Kernel1收到b的值后,將其存到自己Kernel對(duì)應(yīng)的緩存區(qū)中;
- Kernel1接著執(zhí)行while(b==0),因?yàn)榇藭r(shí)b的新值為1,因此跳出循環(huán);
- Kernel1執(zhí)行assert(a==1),由于Kernel1緩存中a的值為0,并且是有效的,所以斷言出錯(cuò);
- Kernel1終于收到了第一步Kernel0發(fā)送的Invalidate了,趕緊將緩存區(qū)的a==1置為invalid,但是為時(shí)已晚。
所以我們看到,這個(gè)例子出錯(cuò)的原因完全是由Store Buffer這個(gè)結(jié)構(gòu)引發(fā)的。如果規(guī)定將Store Buffer中數(shù)據(jù)完全刷入到緩存,才能執(zhí)行對(duì)應(yīng)變量寫操作的話,該錯(cuò)誤也能避免了。
例子2:關(guān)于Invalidate Queue帶來(lái)的錯(cuò)誤,同樣假設(shè)CPU有兩個(gè)核心,Kernel0表示第一個(gè)核心,Kernel1表示第二個(gè)核心。
...
public void foo(){
a=1;
b=1;
}
public void bar(){
while(b==0) continue;
assert(a==1):"a has a wrong value!";
}
...
Kernel0執(zhí)行foo()函數(shù),Kernel1執(zhí)行bar()函數(shù),猜猜看這次斷言會(huì)出錯(cuò)嗎?
假設(shè)在初始情況是這樣的:變量a的值在Kernel0和Kernel1對(duì)應(yīng)的緩存區(qū)都有,狀態(tài)為S(共享),初值為0,變量b的值是0,狀態(tài)為E(獨(dú)占),只存在于Kernel1對(duì)應(yīng)的緩存區(qū),不存在Kernel0對(duì)應(yīng)的緩存區(qū)。假設(shè)Kernel0執(zhí)行foo()函數(shù),Kernel1執(zhí)行bar()函數(shù)時(shí),程序執(zhí)行過程如下:
- Kernel0執(zhí)行a=1,此時(shí)由于a變量被更改了,需要給Kernel1發(fā)送無(wú)效化命令,并且將a的值存儲(chǔ)在Kernel0的Store Buffer中;
- Kernel1執(zhí)行while(b==0),由于Kernel1對(duì)應(yīng)的緩存不包含變量b,它需要發(fā)出一個(gè)讀命令;
- Kernel0執(zhí)行b=1,由于是獨(dú)占的,因此它直接更改自己緩存的值;
- Kernel0收到讀命令,將最新的b的值發(fā)送給Kernel1,并且將變量b的狀態(tài)改變?yōu)镾(共享);
- Kernel1收到Kernel0在第一步發(fā)的無(wú)效化命令,將這個(gè)命令存到Invalidate Queue中,打算之后再處理,并且給Kernel0回確認(rèn)響應(yīng);
- Kernel1收到包含b值的讀命令反饋,把該值存到自己緩存下;
- Kernel1收到b的值之后,打破while循環(huán);
- Kernel1執(zhí)行assert(a==1),由于此時(shí)Invalidate Queue中的無(wú)效化a=0這個(gè)緩存值還沒執(zhí)行,因此Kernel1會(huì)接著用自己緩存中的a=1這個(gè)緩存值,這就出現(xiàn)了問題;
- Kernel1開始執(zhí)行Invalidate Queue中的命令,將a=0這個(gè)緩存值無(wú)效化。但這時(shí)已經(jīng)太晚了。
所以我們看到,這個(gè)例子出錯(cuò)的原因完全是由Invalidate Queue這個(gè)結(jié)構(gòu)引發(fā)的。如果規(guī)定將Invalidate Queue中命令完全處理完,才能執(zhí)行對(duì)應(yīng)變量讀操作的話,該錯(cuò)誤也能避免了。
內(nèi)存屏障
既然剛剛我們遇到了問題,那如何改正呢?這里就終于到了今天的重頭戲,內(nèi)存屏障了。內(nèi)存屏障簡(jiǎn)單來(lái)講就是一行命令,規(guī)定了某個(gè)針對(duì)緩存的操作。這里我們來(lái)看一下最常見的寫屏障和讀屏障。
- 針對(duì)Store Buffer:核心在后續(xù)變量的新值寫入之前,把Store Buffer的所有值刷新到緩存;核心要么就等待刷新完成后寫入,要么就把后續(xù)的后續(xù)變量的新值放到Store Buffer中,直到Store Buffer的數(shù)據(jù)按順序刷入緩存。這種也稱為內(nèi)存屏障中的寫屏障(Store Barrier)。
- 針對(duì)Invalidate Queue:執(zhí)行后需等待Invalidate Queue完全應(yīng)用到緩存后,后續(xù)的讀操作才能繼續(xù)執(zhí)行,保證執(zhí)行前后的讀操作對(duì)其他CPU而言是順序執(zhí)行的。這種也稱為內(nèi)存屏障中的讀屏障(Load Barrier)。
volatile中的內(nèi)存屏障
對(duì)于JVM的內(nèi)存屏障實(shí)現(xiàn)中,也采取了內(nèi)存屏障。JVM的內(nèi)存屏障有四種,這四種實(shí)際上也是上述的讀屏障和寫屏障的組合。我們來(lái)看一下這四種屏障和他們的作用:
-
LoadLoad屏障:對(duì)于這樣的語(yǔ)句
第一大段讀數(shù)據(jù)指令;
LoadLoad;
第二大段讀數(shù)據(jù)指令;
LoadLoad指令作用:在第二大段讀數(shù)據(jù)指令被訪問前,保證第一大段讀數(shù)據(jù)指令執(zhí)行完畢
-
StoreStore屏障:對(duì)于這樣的語(yǔ)句
第一大段寫數(shù)據(jù)指令;
StoreStore;
第二大段寫數(shù)據(jù)指令;
StoreStore指令作用:在第二大段寫數(shù)據(jù)指令被訪問前,保證第一大段寫數(shù)據(jù)指令執(zhí)行完畢
-
LoadStore屏障:對(duì)于這樣的語(yǔ)句
第一大段讀數(shù)據(jù)指令;
LoadStore;
第二大段寫數(shù)據(jù)指令;
LoadStore指令作用:在第二大段寫數(shù)據(jù)指令被訪問前,保證第一大段讀數(shù)據(jù)指令執(zhí)行完畢。
-
StoreLoad屏障:對(duì)于這樣的語(yǔ)句
第一大段寫數(shù)據(jù)指令;
StoreLoad;
第二大段讀數(shù)據(jù)指令;
StoreLoad指令作用:在第二大段讀數(shù)據(jù)指令被訪問前,保證第一大段寫數(shù)據(jù)指令執(zhí)行完畢。
針對(duì)volatile變量,JVM采用的內(nèi)存屏障是:
- 針對(duì)volatile修飾變量的寫操作:在寫操作前插入StoreStore屏障,在寫操作后插入StoreLoad屏障;
- 針對(duì)volatile修飾變量的讀操作:在每個(gè)volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障;
通過這種方式,就可以保證被volatile修飾的變量具有線程間的可見性和禁止指令重排序的功能了。
總結(jié)
講了這么多,我們來(lái)總結(jié)一下。
volatile關(guān)鍵字保證了兩個(gè)性質(zhì):
- 可見性:可見性是指當(dāng)多個(gè)線程訪問同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。
- 有序性:對(duì)一個(gè)volatile變量的寫操作,執(zhí)行在任意后續(xù)對(duì)這個(gè)volatile變量的讀操作之前。
單單緩存一致性協(xié)議無(wú)法實(shí)現(xiàn)volatile。
緩存一致性可以通過Store Buffer和Invalidate Queue兩種結(jié)構(gòu)進(jìn)行加速,但這兩種方式會(huì)造成一系列不一致性的問題。
因此后續(xù)提出了內(nèi)存屏障的概念,分為讀屏障和寫屏障,以此修正Store Buffer和Invalidate Queu產(chǎn)生的問題。
通過讀屏障和寫屏障,又發(fā)展出了LoadLoad屏障,StoreStore屏障,LoadStore屏障,StoreLoad屏障 JVM也是利用了這幾種屏障,實(shí)現(xiàn)volatile關(guān)鍵字。
參考:
- Java多線程編程核心指南
- https://www.jianshu.com/p/ef8de88b1343
- Paul E. McKenney Memory Barriers: a Hardware View for Software Hackers
- https://www.cnblogs.com/xiaolincoding/p/13886559.html
往期推薦
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!