背景
在閱讀java中volatile的關(guān)鍵詞語(yǔ)義時(shí),發(fā)現(xiàn)很多書中都使用了重排序這個(gè)詞來(lái)描述,同時(shí)又講到了線程工作內(nèi)存和主存等等相關(guān)知識(shí)该贾。但是只用那些書的抽象定義進(jìn)行理解時(shí)總是感覺(jué)什么地方說(shuō)不通,最后發(fā)現(xiàn),是那些書中使用的抽象屏蔽了一些對(duì)讀者的知識(shí)點(diǎn)汹押,反而導(dǎo)致了理解上的困難。因此有了這篇文章起便。沒(méi)有任何虛構(gòu)的理解抽象棚贾,從硬件的角度來(lái)理解什么是內(nèi)存屏障,以及內(nèi)存屏障如何讓volatile工作榆综。最后說(shuō)明了在多線程中妙痹,如何使用volatile來(lái)提升性能。
存儲(chǔ)結(jié)構(gòu)
在計(jì)算機(jī)之中奖年,存在著多級(jí)的存儲(chǔ)結(jié)構(gòu)细诸。這是為了適應(yīng)不同硬件速度帶來(lái)的差異。底層是主存(也就是內(nèi)存)陋守,容量最大震贵,速度最慢;中間是cpu的緩存(現(xiàn)代cpu都有多級(jí)緩存結(jié)構(gòu)水评,級(jí)別越高速度越慢猩系,但是可以將多級(jí)緩存看成是一個(gè)整體),容量較小中燥,但是速度很快寇甸;最上層是cpu自身的store buffer和Invalidate Queues,速度最快疗涉,容量非常少拿霉。其中主存和cpu緩存的數(shù)據(jù)視圖對(duì)于每一個(gè)cpu都是相同的,也就是說(shuō)在這個(gè)級(jí)別上咱扣,每個(gè)cpu都看到了相同的數(shù)據(jù)绽淘,而store buffer和Invalidate Queues是每一個(gè)cpu私有的。這就導(dǎo)致了一系列的編程問(wèn)題闹伪,下文會(huì)詳細(xì)展開沪铭。
CPU緩存
Cpu為了平衡自身處理速度過(guò)快和主存讀寫速度過(guò)慢這個(gè)問(wèn)題壮池,使用了緩存來(lái)存儲(chǔ)處理中的熱點(diǎn)數(shù)據(jù)。cpu需要處理數(shù)據(jù)的時(shí)候(包含讀取和寫出)杀怠,都是直接向緩存發(fā)出讀寫指令椰憋。如果數(shù)據(jù)不在緩存中,則會(huì)從主存中讀取數(shù)據(jù)到緩存中赔退,再做對(duì)應(yīng)的處理橙依。需要注意的是,cpu讀取數(shù)據(jù)到緩存中离钝,是固定長(zhǎng)度的讀取票编。也就是說(shuō)cpu緩存是一行一行的載入數(shù)據(jù)進(jìn)來(lái)。因?yàn)橐卜Q之為cpu緩存行卵渴,即cacheline慧域。而緩存中的數(shù)據(jù),也會(huì)在合適的時(shí)候回寫到主存當(dāng)中(這個(gè)時(shí)機(jī)可以抽象的認(rèn)為是由cpu自行決定的)浪读。
現(xiàn)在的cpu都是多核cpu昔榴,為了在處理數(shù)據(jù)的時(shí)候保持緩存有效性,因此一個(gè)cpu需要數(shù)據(jù)而且該數(shù)據(jù)不在自身的cache中的時(shí)候碘橘,會(huì)同時(shí)向其他的cpu緩存和主存求取互订。如果其他的cpu緩存中有數(shù)據(jù),則使用該數(shù)據(jù)痘拆,這樣就保證了不會(huì)使用到主存中的錯(cuò)誤的尚未更新的舊數(shù)據(jù)仰禽。
而各個(gè)cpu的內(nèi)部緩存依靠MESI緩存一致性協(xié)議來(lái)進(jìn)行協(xié)調(diào)。以此保證各個(gè)Cpu看到的內(nèi)容是一致的纺蛆。
Store buffer
如果一個(gè)Cpu要寫出一個(gè)數(shù)據(jù)吐葵,但是此時(shí)這個(gè)數(shù)據(jù)不在自己的cacheline中,因?yàn)閏pu要向其他的cpu緩存發(fā)出read invalidate消息桥氏。等待其他的cpu返回read response和invalidate ack消息后温峭,將數(shù)據(jù)寫入這個(gè)cacheline。這里就存在著時(shí)間的浪費(fèi)字支,因?yàn)椴还芷渌腸pu返回的是什么數(shù)據(jù)凤藏,本cpu都是要將它覆蓋的。而在等待的這段時(shí)間堕伪,cpu無(wú)事可干揖庄,只能空轉(zhuǎn)。為了讓cpu不至于空閑欠雌,因?yàn)樵O(shè)計(jì)了store buffer組件抠艾。store buffer是每個(gè)cpu獨(dú)享的寫入緩存空間,用于存儲(chǔ)對(duì)cacheline的寫入桨昙,而且速度比cacheline高一個(gè)數(shù)量級(jí)检号,但是容量非常少。
但是store buffer會(huì)產(chǎn)生在單核上的讀寫不一致問(wèn)題蛙酪。下面是模擬
a = 1;
b = a+1;
assert b==2;
假設(shè)a不在本cpu的cacheline中齐苛。在其他cpu的cacheline中,值為0.會(huì)有如下的步驟
序號(hào) | 操作內(nèi)容 |
---|---|
1 | 發(fā)現(xiàn)a的地址不在本cpu的cacheline中桂塞,向其他的cpu發(fā)送read invalidate消息 |
2 | 將數(shù)據(jù)寫入store buffer中 |
3 | 收到其他cpu響應(yīng)的read response和invalidate ack消息 |
4 | 執(zhí)行b=a+1凹蜂,因?yàn)閍這個(gè)時(shí)候已經(jīng)在cacheline中,讀取到值為0阁危,加1后為1玛痊,寫入到b中 |
5 | 執(zhí)行assert b==2 失敗,因?yàn)閎是1 |
6 | store buffer中的值刷新到a的cacheline中狂打,修改a的值為1擂煞,但是已經(jīng)太晚了 |
為了避免這個(gè)問(wèn)題,所以對(duì)于store buffer的設(shè)計(jì)中增加一個(gè)策略叫做store forwarding趴乡。就是說(shuō)cpu在讀取數(shù)據(jù)的時(shí)候會(huì)先查看store buffer对省,如果store buffer中有數(shù)據(jù),直接用store buffer中的晾捏。這樣蒿涎,也就避免了使用錯(cuò)誤數(shù)據(jù)的問(wèn)題了。
store forwarding可以解決在單線程中的數(shù)據(jù)不一致問(wèn)題惦辛,但是store buffer所帶來(lái)的復(fù)雜性遠(yuǎn)不止如此劳秋。在多線程環(huán)境下,會(huì)有其他的問(wèn)題胖齐。下面是模擬代碼
public void set(){
a=1;
b=1;
}
public void print(){
while(b==0)
;
assert a==1;
}
假設(shè)a和b的值都是0玻淑,其中b在cpu0中,a在cpu1中市怎。cpu0執(zhí)行set方法岁忘,cpu1執(zhí)行print方法。
序號(hào) | cpu0的步驟(執(zhí)行set) | cpu1的步驟(執(zhí)行print) |
---|---|---|
1 | 想寫入a=1区匠,但是由于a不在自身的cacheline中干像,向cpu1發(fā)送read invalidate消息 | 執(zhí)行while(b==0),由于b不在自身的cacheline中,向cpu0發(fā)送read消息 |
2 | 向store buffer中寫入a=1 | 等待cpu0響應(yīng)的read response消息 |
3 | b在自身的cacheline中驰弄,并且此時(shí)狀態(tài)為M或者E麻汰,寫入b=1 | 等待cpu0響應(yīng)的read response消息 |
4 | 收到cpu1的read請(qǐng)求,將b=1的值用read response消息傳遞戚篙,同時(shí)將b所在的cacheline修改狀態(tài)為s | 等待cpu0響應(yīng)的read response消息 |
5 | 等待cpu1的read response和invalidate ack消息 | 收到cpu0的read response消息五鲫,將b置為1,因此程序跳出循環(huán) |
6 | 等待cpu1的read response和invalidate ack消息 | 因?yàn)閍在自身的cacheline中岔擂,所以讀取后進(jìn)行比對(duì)位喂。assert a==1失敗浪耘。因?yàn)榇藭r(shí)a在自身cacheline中的值還是0,而且該cacheline尚未失效 |
7 | 等待cpu1的read response和invalidate ack消息 | 收到cpu0發(fā)送的read invalidate消息塑崖,將a所在的cacheline設(shè)置為無(wú)效七冲,但是 為時(shí)已晚,錯(cuò)誤的判斷結(jié)果已經(jīng)產(chǎn)生了 |
8 | 收到cpu1響應(yīng)的read response和invalidate ack消息规婆,將store buffer中的值寫入cacheline中 | 無(wú) |
通過(guò)上面的例子可以看到澜躺,在多核系統(tǒng)中,store buffer的存在讓程序的結(jié)果與我們的預(yù)期不相符合抒蚜。上面的程序中掘鄙,由于store buffer的存在,所以在cacheline中的操作順序?qū)嶋H上先b=1然后a=1嗡髓。就好像操作被重排序一樣(重排序這個(gè)詞在很多文章中都有操漠,但是定義不詳,不好理解器贩。實(shí)際上直接理解store buffer會(huì)簡(jiǎn)單很多)颅夺。為了解決這樣的問(wèn)題,cpu提供了一些操作指令蛹稍,來(lái)幫助我們避免這樣的問(wèn)題吧黄。這樣的指令就是內(nèi)存屏障(英文fence,也翻譯叫做柵欄)唆姐。來(lái)看下面的代碼
public void set(){
a=1;
smp_mb();
b=1;
}
public void print(){
while(b==0)
;
assert a==1;
}
smb_mb()
就是內(nèi)存屏障指令拗慨,英文memory barries。它的作用奉芦,是在后續(xù)的store動(dòng)作之前赵抢,將sotre buffer中的內(nèi)容刷新到cacheline。這個(gè)操作的效果是讓本地的cacheline的操作順序和代碼的順序一致声功,也就是讓其他cpu觀察到的該cpu的cacheline操作順序被分為smp_mb()之前和之后烦却。要達(dá)到這個(gè)目的有兩種方式
- 遇到smp_mb()指令時(shí),暫停cpu執(zhí)行先巴,將當(dāng)前的store_buffer全部刷新到cacheline中其爵,完成后cpu繼續(xù)執(zhí)行
- 遇到smp_mb()指令時(shí),cpu繼續(xù)執(zhí)行伸蚯,但是所有后續(xù)的store操作都進(jìn)入到了store buffer中摩渺,直到store buffer之前的內(nèi)容都被刷新到cacheline,即使此時(shí)需要store的內(nèi)容的cacheline是M或者E狀態(tài)剂邮,也只能先寫入store buffer中摇幻。這樣的策略,既可以提升cpu效率绰姻,也保證了正確性枉侧。當(dāng)之前store buffer的內(nèi)容被刷新到cacheline完成后银酗,后面新增加的內(nèi)容也會(huì)有合適的時(shí)機(jī)刷新到cacheline。把store buffer想象成一個(gè)FIFO的隊(duì)列就可以了。
下面來(lái)看,當(dāng)有了smp_mb()
之后瞳遍,程序的執(zhí)行情況掠械。所有的初始假設(shè)與上面相同猾蒂。
序號(hào) | cpu0的步驟(執(zhí)行set) | cpu1的步驟(執(zhí)行print) |
---|---|---|
1 | 想寫入a=1,但是由于a不在自身的cacheline中蚊逢,向cpu1發(fā)送read invalidate消息 | 執(zhí)行while(b==0),由于b不在自身的cacheline中时捌,向cpu0發(fā)送read消息 |
2 | 向store buffer中寫入a=1 | 等待cpu0響應(yīng)的read response消息 |
3 | 遇到smp_mb()扒袖,等待直到可以將store buffer中的內(nèi)容刷新到cacheline | 等待cpu0響應(yīng)的read response消息 |
4 | 等待直到可以將store buffer中的內(nèi)容刷新到cacheline | 收到cpu0發(fā)來(lái)的read invalidate消息,發(fā)送a=0的值,同時(shí)將自身a所在的cacheline修改為invalidate狀態(tài) |
5 | 收到cpu1響應(yīng)的read response和invalidate ack消息,將a=0的值設(shè)置到cacheline,隨后store buffer中a=1的值刷新到cacheline聊倔,設(shè)置cacheline狀態(tài)為M | 等待cpu0響應(yīng)的read response消息 |
6 | 由于b就在自身的cacheline中,并且狀態(tài)為M或者E邀层,設(shè)置值為b=1 | 等待cpu0響應(yīng)的read response消息 |
7 | 收到cpu1的read請(qǐng)求,將b=1的值傳遞回去垮兑,同時(shí)設(shè)置該cacheline狀態(tài)為s | 等待cpu0響應(yīng)的read response消息 |
8 | 無(wú) | 收到cpu0的read response信息垢村,將b設(shè)置為1倍谜,程序跳出循環(huán) |
9 | 無(wú) | 由于a所在的cacheline被設(shè)置為invalidate季春,因此向cpu0發(fā)送read請(qǐng)求 |
10 | 收到cpu1的read請(qǐng)求逞刷,以a=1響應(yīng)番枚,并且將自身的cacheline狀態(tài)修改為s | 等待cpu0的read response響應(yīng) |
11 | 無(wú) | 收到read response請(qǐng)求路星,將a設(shè)置為1,執(zhí)行程序判斷缰冤,結(jié)果為真 |
可以看到焚碌,在有了內(nèi)存屏障之后寝殴,程序的真實(shí)結(jié)果就和我們的預(yù)期結(jié)果相同了臭猜。
invalidate queue
使用了store buffer后揽碘,cpu的store性能會(huì)提升很多次屠。然后store buffer的容量是很小的(越快的東西园匹,成本就越高,一定就越薪僭睢)裸违,cpu以中等的頻率填充store buffer。如果不幸發(fā)生比較多的cache miss本昏,那么很快store buffer就被填滿了供汛,cpu只能等待。又或者程序中調(diào)用了smp_mb()
指令凛俱,這樣后續(xù)的操作都只能進(jìn)入store buffer紊馏,而不管相關(guān)cacheline是否處于M或者E狀態(tài)。
store buffer很容易滿的原因是因?yàn)槭盏狡渌鹀pu的invalidate ack的速度太慢蒲犬。而cpu發(fā)送invalidate ack的速度太慢是因?yàn)閏pu要等到將對(duì)應(yīng)的cacheline設(shè)置為invalidate后才能發(fā)送invalidate ack朱监。有的時(shí)候太多invalidate請(qǐng)求,cpu的處理速度就跟不上原叮。為了加速這個(gè)流程赫编,硬件設(shè)計(jì)者設(shè)計(jì)了invaldate queue來(lái)加速這個(gè)過(guò)程。收到的invalidate請(qǐng)求先放入invalidate queue奋隶,然后之后立刻響應(yīng)invalidate ack消息擂送。而cpu可以在隨后慢慢的處理這些invalidate消息。當(dāng)然唯欣,這里必須不能太慢嘹吨。也就是說(shuō),cpu實(shí)際上給出了一個(gè)承諾境氢,如果一個(gè)invalidatge請(qǐng)求在invalidate queue中蟀拷,那么對(duì)于這個(gè)請(qǐng)求相關(guān)的cacheline,在該請(qǐng)求被處理完成前萍聊,cpu不會(huì)再發(fā)送任何與該cacheline相關(guān)的MESI消息问芬。在有了store buffer和invalidate queue后,cpu的處理速度又可以更高寿桨。下面是結(jié)構(gòu)圖此衅。
但是在引入了invalidate queue又會(huì)導(dǎo)致另外一個(gè)問(wèn)題。下面先來(lái)看代碼
public void set(){
a=1;
smp_mb();
b=1;
}
public void print(){
while(b==0)
;
assert a==1;
}
代碼與上面的例子相同亭螟,但是初始條件不同了挡鞍。這次a同時(shí)存在于cpu0和cpu1之中,狀態(tài)為s预烙。b是cpu0獨(dú)享匕累,狀態(tài)為E或者M(jìn)。
序號(hào) | cpu0的步驟(執(zhí)行set) | cpu1的步驟(執(zhí)行print) |
---|---|---|
1 | 想寫入a=1默伍,但是由于a的狀態(tài)是s欢嘿,向cpu1發(fā)送invalidate消息 | 執(zhí)行while(b==0),由于b不在自身的cacheline中衰琐,向cpu0發(fā)送read消息 |
2 | 向store buffer中寫入a=1 | 收到cpu0的invalidate消息,放入invalidate queue炼蹦,響應(yīng)invalidate ack消息羡宙。 |
3 | 遇到smp_mb(),等待直到可以將store buffer中的內(nèi)容刷新到cacheline掐隐。立刻收到cpu0的invalidate ack狗热,將store buffer中的a=1寫入到cacheline,并且修改狀態(tài)為M | 等待cpu0響應(yīng)的read response消息 |
4 | 由于b就在自己的cacheline中虑省,寫入b=1匿刮,修改狀態(tài)為M | 等待cpu0響應(yīng)的read response消息 |
5 | 收到cpu1響應(yīng)的read請(qǐng)求,將b=1作為響應(yīng)回傳探颈,同時(shí)將cacheline的狀態(tài)修改為s熟丸。 | 等待cpu0響應(yīng)的read response消息 |
6 | 無(wú) | 收到read response,將b=1寫入cacheline伪节,程序跳出循環(huán) |
7 | 無(wú) | 由于a所在的cacheline還未失效光羞,load值,進(jìn)行比對(duì)怀大,assert失敗 |
8 | 無(wú) | cpu處理invalidate queue的消息纱兑,將a所在的cacheline設(shè)置為invalidate,但是已經(jīng)太晚了 |
上面的例子化借,看起來(lái)就好像第一個(gè)一樣潜慎,仍然是b=1先生效,a=1后生效蓖康。導(dǎo)致了cpu1執(zhí)行的錯(cuò)誤勘纯。就好像內(nèi)存操作”重排序”一樣(個(gè)人不太喜歡內(nèi)存操作重排序這個(gè)術(shù)語(yǔ),因?yàn)閷?shí)際上并不是重新排序的問(wèn)題钓瞭,而是是否可見的問(wèn)題。但是用重排序這樣的詞語(yǔ)淫奔,反而不好理解山涡。但是很多書都是用是了這個(gè)詞語(yǔ),大家可以有自己的理解唆迁。但是還是推薦不要理會(huì)這些作者的抽象概念鸭丛,直接了解核心)。其實(shí)這個(gè)問(wèn)題的觸發(fā)唐责,就是因?yàn)閕nvalidate queue沒(méi)有在需要被處理的時(shí)候處理完成鳞溉,造成了原本早該失效的cacheline仍然被cpu認(rèn)為是有效,出現(xiàn)了錯(cuò)誤的結(jié)果鼠哥。那么只要讓內(nèi)存屏障增加一個(gè)讓invalidate queue全部處理完成的功能即可熟菲。
硬件的設(shè)計(jì)者也是這么考慮的看政,請(qǐng)看下面的代碼
public void set(){
a=1;
smp_mb();
b=1;
}
public void print(){
while(b==0)
;
smp_mb();
assert a==1;
}
a同時(shí)存在于cpu0和cpu1之中,狀態(tài)為s抄罕。b是cpu0獨(dú)享允蚣,狀態(tài)為E或者M(jìn)。
序號(hào) | cpu0的步驟(執(zhí)行set) | cpu1的步驟(執(zhí)行print) |
---|---|---|
1 | 想寫入a=1呆贿,但是由于a的狀態(tài)是s嚷兔,向cpu1發(fā)送invalidate消息 | 執(zhí)行while(b==0),由于b不在自身的cacheline中,向cpu0發(fā)送read消息 |
2 | 向store buffer中寫入a=1 | 收到cpu0的invalidate消息做入,放入invalidate queue冒晰,響應(yīng)invalidate ack消息。 |
3 | 遇到smp_mb()竟块,等待直到可以將store buffer中的內(nèi)容刷新到cacheline壶运。立刻收到cpu0的invalidate ack,將store buffer中的a=1寫入到cacheline彩郊,并且修改狀態(tài)為M | 等待cpu0響應(yīng)的read response消息 |
4 | 由于b就在自己的cacheline中前弯,寫入b=1,修改狀態(tài)為M | 等待cpu0響應(yīng)的read response消息 |
5 | 收到cpu1響應(yīng)的read請(qǐng)求秫逝,將b=1作為響應(yīng)回傳恕出,同時(shí)將cacheline的狀態(tài)修改為s。 | 等待cpu0響應(yīng)的read response消息 |
6 | 無(wú) | 收到read response违帆,將b=1寫入cacheline浙巫,程序跳出循環(huán) |
7 | 無(wú) | 遇見smp_mb(),讓cpu將invalidate queue中的消息全部處理完后刷后,才能繼續(xù)向下執(zhí)行的畴。此時(shí)將a所在的cacheline設(shè)置為invalidate |
8 | 無(wú) 由于a所在的cacheline已經(jīng)無(wú)效,向cpu0發(fā)送read消息 | |
9 | 收到read請(qǐng)求尝胆,以a=1發(fā)送響應(yīng) | 收到cpu0發(fā)送的響應(yīng)丧裁,以a=1寫入cacheline,執(zhí)行assert a==1.判斷成功 |
可以看到含衔,由于內(nèi)存屏障的加入煎娇,程序正確了。
內(nèi)存屏障
通過(guò)上面的解釋和例子贪染,可以看出缓呛,內(nèi)存屏障是是因?yàn)橛辛藄tore buffer和invalidate queue之后,被用來(lái)解決可見性問(wèn)題(也就是在cacheline上的操作重排序問(wèn)題)杭隙。內(nèi)存屏障具備兩方面的作用
- 強(qiáng)制cpu將store buffer中的內(nèi)容寫入到cacheline中
- 強(qiáng)制cpu將invalidate queue中的請(qǐng)求處理完畢
但是有些時(shí)候哟绊,我們只需要其中一個(gè)功能即可,所以硬件設(shè)計(jì)者們就將功能細(xì)化痰憎,分別是
- 讀屏障: 強(qiáng)制cpu將invalidate queue中的請(qǐng)求處理完畢票髓。也被稱之為smp_rmb
- 寫屏障: 強(qiáng)制cpu將store buffer中的內(nèi)容寫入到cacheline中或者將該指令之后的寫操作寫入store buffer直到之前的內(nèi)容被寫入cacheline.也被稱之為smp_wmb
- 讀寫屏障: 強(qiáng)制刷新store buffer中的內(nèi)容到cacheline攀涵,強(qiáng)制cpu處理完invalidate queue中的內(nèi)容。也被稱之為smp_mb
JMM內(nèi)存模型
在上面描述中可以看到硬件為我們提供了很多的額外指令來(lái)保證程序的正確性炬称。但是也帶來(lái)了復(fù)雜性汁果。JMM為了方便我們理解和使用,提供了一些抽象概念的內(nèi)存屏障玲躯。注意据德,下文開始討論的內(nèi)存屏障都是指的是JMM的抽象內(nèi)存屏障,它并不代表實(shí)際的cpu操作指令跷车,而是代表一種效果棘利。
-
LoadLoad Barriers
該屏障保證了在屏障前的讀取操作效果先于屏障后的讀取操作效果發(fā)生。在各個(gè)不同平臺(tái)上會(huì)插入的編譯指令不相同朽缴,可能的一種做法是插入也被稱之為smp_rmb指令善玫,強(qiáng)制處理完成當(dāng)前的invalidate queue中的內(nèi)容 -
StoreStore Barriers
該屏障保證了在屏障前的寫操作效果先于屏障后的寫操作效果發(fā)生∶芮浚可能的做法是使用smp_wmb指令茅郎,而且是使用該指令中,將后續(xù)寫入數(shù)據(jù)先寫入到store buffer的那種處理方式或渤。因?yàn)檫@種方式消耗比較小 - ** LoadStore Barriers**
該屏障保證了屏障前的讀操作效果先于屏障后的寫操作效果發(fā)生系冗。 -
StoreLoad Barriers
該屏障保證了屏障前的寫操作效果先于屏障后的讀操作效果發(fā)生。該屏障兼具上面三者的功能薪鹦,是開銷最大的一種屏障掌敬。可能的做法就是插入一個(gè)smp_mb指令來(lái)完成池磁。
內(nèi)存屏障在volatile關(guān)鍵中的使用
內(nèi)存屏障在很多地方使用奔害,這里主要說(shuō)下對(duì)于volatile關(guān)鍵字,內(nèi)存屏障的使用方式地熄。
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障华临。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障端考。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障雅潭。
上面的內(nèi)存屏障方式主要是規(guī)定了在處理器級(jí)別的一些重排序要求。而JMM本身跛梗,對(duì)于volatile變量在編譯器級(jí)別的重排序也制定了相關(guān)的規(guī)則∑迕郑可以用下面的圖來(lái)表示
volatile變量除了在編譯器重排序方面的語(yǔ)義以外核偿,還存在一條約束保證。如果cpu硬件上存在類似invalidate queue的東西顽染,可以在進(jìn)行變量讀取操作之前漾岳,會(huì)先處理完畢queue上的內(nèi)容轰绵。這樣就能保證volatile變量始終是讀取最新的最后寫入的值。
Happen-before
JMM為了簡(jiǎn)化對(duì)編程復(fù)雜的理解尼荆,使用了HB來(lái)表達(dá)不同操作之間的可見性左腔。HB關(guān)系在不同的書籍中有不同的表達(dá)。這里推薦一種比較好理解的捅儒。
A Happen before B,說(shuō)明A操作的效果先于B操作的效果發(fā)生液样。這種偏序關(guān)系在單線程中是沒(méi)有什么作用的,因?yàn)閱尉€程中巧还,執(zhí)行效果要求和代碼順序一致鞭莽。但是在多線程中,其可見性作用就非常明顯了麸祷。舉個(gè)例子澎怒,在線程1中進(jìn)行進(jìn)行a,b操作阶牍,操作存在hb關(guān)系喷面。那么當(dāng)線程2觀察到b操作的效果時(shí),必然也能觀察到a操作的效果走孽,因?yàn)閍操作Happen before b操作惧辈。
在java中,存在HB關(guān)系的操作一共有8種融求,如下咬像。
- 程序次序法則,如果A一定在B之前發(fā)生生宛,則happen before
- 監(jiān)視器法則,對(duì)一個(gè)監(jiān)視器的解鎖一定發(fā)生在后續(xù)對(duì)同一監(jiān)視器加鎖之前
- Volatie變量法則:寫volatile變量一定發(fā)生在后續(xù)對(duì)它的讀之前
- 線程啟動(dòng)法則:Thread.start一定發(fā)生在線程中的動(dòng)作前
- 線程終結(jié)法則:線程中的任何動(dòng)作一定發(fā)生在線程終結(jié)之前(其他線程 檢測(cè)到這個(gè)線程已經(jīng)終止县昂,從Thread.join調(diào)用成功返回,Thread.isAlive()返回false)
6.中斷法則:一個(gè)線程調(diào)用另一個(gè)線程的interrupt一定發(fā)生在另一線程發(fā)現(xiàn)中斷之前陷舅。
7.終結(jié)法則:一個(gè)對(duì)象的構(gòu)造函數(shù)結(jié)束一定發(fā)生在對(duì)象的finalizer之前
8.傳遞性:A發(fā)生在B之前倒彰,B發(fā)生在C之前,A一定發(fā)生在C之前莱睁。
使用HB關(guān)系待讳,在多線程開發(fā)時(shí)就可以盡量少的避免使用鎖,而是直接利用hb關(guān)系和volatile關(guān)鍵字來(lái)達(dá)到信息傳遞并且可見的目的仰剿。
比如很常見的一個(gè)線程處理一些數(shù)據(jù)并且修改標(biāo)識(shí)位后创淡,另外的線程檢測(cè)到標(biāo)識(shí)位發(fā)生改變,就接手后續(xù)的流程南吮。此時(shí)如何保證前一個(gè)線程對(duì)數(shù)據(jù)做出的更改后一個(gè)線程全部可見呢琳彩。先來(lái)看下面的代碼例子
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
while(flag==false); //3
int i=a; //4
}
}
有兩個(gè)不同的線程分別執(zhí)行writer和reader方法,根據(jù)Hb規(guī)則,有如下的順序執(zhí)行圖露乏。
[圖片上傳失敗...(image-3af791-1537152232727)]
這樣的順序碧浊,i讀取到的a的值就是最新的,也即是1.