最近又看了下Disruptor套媚,里面提到了內(nèi)存屏障缚态,突然想到了指令重排、還有可見性堤瘤,感覺里面關系有點亂玫芦,就翻了下,因此就寫了這篇文章
帶著幾個問題:
- 1.volatile本辐,是怎么可見性的問題(CPU緩存)桥帆,那么他是怎么解決的--->MESI
- 2.CAS指令,確保了對同一個同一個內(nèi)存地址操作的原子性慎皱,那么他應該也會遇到和上面可見性一樣的問題老虫,他是怎么解決的,是不是和volatile的底層原理類似茫多?--->是的祈匙,也是利用了MESI
- 3.volatile還避免了指令重排,是通過內(nèi)存屏障解決的天揖?那么他和MESI有什么關系夺欲?還是說volatile關鍵字即用了MESI也用了內(nèi)存屏障?--->是的今膊,其實MESI底層也還是需要內(nèi)存屏障
一些阅、可見性和MESI
1.1 可見性
在JVM的內(nèi)存模型中,每個線程有自己的工作內(nèi)存斑唬,實際上JAVA線程借助了底層操作系統(tǒng)線程實現(xiàn)市埋,一個JVM線程對應一個操作系統(tǒng)線程,線程的工作內(nèi)存其實是cpu寄存器和高速緩存的抽象
現(xiàn)代處理器的緩存一般分為三級恕刘,由每一個核心獨享的L1缤谎、L2 Cache,以及所有的核心共享L3 Cache組成雪营,具體每個cache弓千,實際上是有很多緩存行組成:
1.2 緩存一致性和MESI
緩存一致性協(xié)議給緩存行(通常為64字節(jié))定義了個狀態(tài):獨占(exclusive)、共享(share)献起、修改(modified)洋访、失效(invalid),用來描述該緩存行是否被多處理器共享谴餐、是否修改姻政。所以緩存一致性協(xié)議也稱MESI協(xié)議。
- 獨占(exclusive):僅當前處理器擁有該緩存行岂嗓,并且沒有修改過汁展,是最新的值。
- 共享(share):有多個處理器擁有該緩存行,每個處理器都沒有修改過緩存食绿,是最新的值侈咕。
- 修改(modified):僅當前處理器擁有該緩存行,并且緩存行被修改過了器紧,一定時間內(nèi)會寫回主存耀销,會寫成功狀態(tài)會變?yōu)镾。
- 失效(invalid):緩存行被其他處理器修改過铲汪,該值不是最新的值熊尉,需要讀取主存上最新的值。
協(xié)議協(xié)作如下:
- 一個處于M狀態(tài)的緩存行掌腰,必須時刻監(jiān)聽所有試圖讀取該緩存行對應的主存地址的操作狰住,如果監(jiān)聽到,則必須在此操作執(zhí)行前把其緩存行中的數(shù)據(jù)寫回CPU齿梁。
- 一個處于S狀態(tài)的緩存行催植,必須時刻監(jiān)聽使該緩存行無效或者獨享該緩存行的請求,如果監(jiān)聽到士飒,則必須把其緩存行狀態(tài)設置為I查邢。
- 一個處于E狀態(tài)的緩存行,必須時刻監(jiān)聽其他試圖讀取該緩存行對應的主存地址的操作酵幕,如果監(jiān)聽到,則必須把其緩存行狀態(tài)設置為S缓苛。
- 當CPU需要讀取數(shù)據(jù)時芳撒,如果其緩存行的狀態(tài)是I的,則需要從內(nèi)存中讀取未桥,并把自己狀態(tài)變成S笔刹,如果不是I,則可以直接讀取緩存中的值冬耿,但在此之前舌菜,必須要等待其他CPU的監(jiān)聽結(jié)果,如其他CPU也有該數(shù)據(jù)的緩存且狀態(tài)是M亦镶,則需要等待其把緩存更新到內(nèi)存之后日月,再讀取。
- 當CPU需要寫數(shù)據(jù)時缤骨,只有在其緩存行是M或者E的時候才能執(zhí)行爱咬,否則需要發(fā)出特殊的RFO指令(Read Or Ownership,這是一種總線事務)绊起,通知其他CPU置緩存無效(I)精拟,這種情況下會性能開銷是相對較大的。在寫入完成后,修改其緩存狀態(tài)為M蜂绎。
這個圖的含義就是當一個core持有一個cacheline的狀態(tài)為Y時,其它core對應的cacheline應該處于狀態(tài)X, 比如地址 0x00010000 對應的cacheline在core0上為狀態(tài)M, 則其它所有的core對應于0x00010000的cacheline都必須為I , 0x00010000 對應的cacheline在core0上為狀態(tài)S, 則其它所有的core對應于0x00010000的cacheline 可以是S或者I ,
另外MESI協(xié)議為了提高性能栅表,引入了Store Buffe和Invalidate Queues,還是有可能會引起緩存不一致师枣,還會再引入內(nèi)存屏障來確保一致性谨读,可以參考[7]和[12]
存儲緩存(Store Buffe)
也就是常說的寫緩存,當處理器修改緩存時坛吁,把新值放到存儲緩存中劳殖,處理器就可以去干別的事了,把剩下的事交給存儲緩存拨脉。
失效隊列(Invalidate Queues)
處理失效的緩存也不是簡單的哆姻,需要讀取主存。并且存儲緩存也不是無限大的玫膀,那么當存儲緩存滿的時候矛缨,處理器還是要等待失效響應的。為了解決上面兩個問題帖旨,引進了失效隊列(invalidate queue)箕昭。處理失效的工作如下:
- 收到失效消息時,放到失效隊列中去解阅。
- 為了不讓處理器久等失效響應落竹,收到失效消息需要馬上回復失效響應。
- 為了不頻繁阻塞處理器货抄,不會馬上讀主存以及設置緩存為invlid述召,合適的時候再一塊處理失效隊列。
1.3 MESI和CAS關系
在x86架構(gòu)上蟹地,CAS被翻譯為”lock cmpxchg...“积暖,當兩個core同時執(zhí)行針對同一地址的CAS指令時,其實他們是在試圖修改每個core自己持有的Cache line,
假設兩個core都持有相同地址對應cacheline,且各自cacheline 狀態(tài)為S, 這時如果要想成功修改,就首先需要把S轉(zhuǎn)為E或者M, 則需要向其它core invalidate 這個地址的cacheline,則兩個core都會向ring bus發(fā)出 invalidate這個操作, 那么在ringbus上就會根據(jù)特定的設計協(xié)議仲裁是core0,還是core1能贏得這個invalidate, 勝者完成操作, 失敗者需要接受結(jié)果, invalidate自己對應的cacheline,再讀取勝者修改后的值, 回到起點.
對于我們的CAS操作來說, 其實鎖并沒有消失,只是轉(zhuǎn)嫁到了ring bus的總線仲裁協(xié)議中. 而且大量的多核同時針對一個地址的CAS操作會引起反復的互相invalidate 同一cacheline, 造成pingpong效應, 同樣會降低性能(參考[9])。當然如果真的有性能問題怪与,我覺得這可能會在ns級別體現(xiàn)了,一般的應用程序中使用CAS應該不會引起性能問題
二夺刑、指令重排和內(nèi)存屏障
2.1 指令重排
現(xiàn)代CPU的速度越來越快,為了充分的利用CPU分别,在編譯器和CPU執(zhí)行期遍愿,都可能對指令重排。舉個例子:
LDR R1, [R0];//操作1
ADD R2, R1, R1;//操作2
ADD R3, R4, R4;//操作3
上面這段代碼茎杂,如果操作1如果發(fā)生cache miss错览,則需要等待讀取內(nèi)存外存』屯看看有沒有能優(yōu)先執(zhí)行的指令倾哺,操作2依賴于操作1轧邪,不能被優(yōu)先執(zhí)行,操作3不依賴1和2羞海,所以能優(yōu)先執(zhí)行操作3忌愚。
JVM的JSR-133規(guī)范中定義了as-if-serial語義,即compiler, runtime, and hardware三者需要保證在單線程模型下程序不會感知到指令重排的影響却邓。
在并發(fā)模型下硕糊,重排序還是可能會引發(fā)問題,比較經(jīng)典的就是“單例模式失效”問題(DoubleCheckedLocking):
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
instance = new Singleton(); //
}
}
}
return instance;
}
}
上面這段代碼腊徙,初看沒問題简十,但是在并發(fā)模型下,可能會出錯,那是因為instance= new Singleton()并非一個原子操作撬腾,它實際上下面這三個操作:
memory =allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance =memory; //3:設置instance指向剛分配的內(nèi)存地址
上面操作2依賴于操作1螟蝙,但是操作3并不依賴于操作2,所以JVM是可以針對它們進行指令的優(yōu)化重排序的民傻,經(jīng)過重排序后如下:
memory =allocate(); //1:分配對象的內(nèi)存空間
instance =memory; //3:instance指向剛分配的內(nèi)存地址胰默,此時對象還未初始化
ctorInstance(memory); //2:初始化對象
可以看到指令重排之后,instance指向分配好的內(nèi)存放在了前面漓踢,而這段內(nèi)存的初始化被排在了后面牵署。在多線程場景下,可能A線程執(zhí)行到了3喧半,B線程發(fā)現(xiàn)已經(jīng)不為空就返回繼續(xù)執(zhí)行验烧,就會出錯寨辩。
在java里面volatile可以防止重排划址,當然還有另外一個作用即內(nèi)存可見性实抡,這個知道的人還應該比較普遍兴垦,就不說了
2.2 內(nèi)存屏障
硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障如暖。內(nèi)存屏障有兩個作用:
1.阻止屏障兩側(cè)的指令重排序率挣;
2.強制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存清寇,讓緩存中相應的數(shù)據(jù)失效浩村。
在JSR規(guī)范中定義了4種內(nèi)存屏障:
- LoadLoad屏障:(指令Load1; LoadLoad; Load2)做葵,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢心墅。
- LoadStore屏障:(指令Load1; LoadStore; Store2)酿矢,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢怎燥。
- StoreStore屏障:(指令Store1; StoreStore; Store2)瘫筐,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見铐姚。
- StoreLoad屏障:(指令Store1; StoreLoad; Load2)策肝,在Load2及后續(xù)所有讀取操作執(zhí)行前肛捍,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的之众。在大多數(shù)處理器的實現(xiàn)中拙毫,這個屏障是個萬能屏障,兼具其它三種內(nèi)存屏障的功能
對于volatile關鍵字棺禾,按照規(guī)范會有下面的操作:
- 在每個volatile寫入之前缀蹄,插入一個StoreStore,寫入之后膘婶,插入一個StoreLoad
- 在每個volatile讀取之前缺前,插入LoadLoad,之后插入LoadStore
具體到X86來看悬襟,其實沒那么多指令衅码,只有StoreLoad:
結(jié)合上面的【一】和【二】的內(nèi)容,內(nèi)存屏障首先阻止了指令的重排古胆,另外也和MESI協(xié)議結(jié)合肆良,確保了內(nèi)存的可見性
三、happends-before
結(jié)合前面的兩點逸绎,再看happends-before就比較好理解了惹恃。因為光說可見性和重排很難聯(lián)想到happends-before。這個點在并發(fā)編程里還是非常重要的棺牧,再詳細記錄下:
- 1.Each action in a thread happens-before every subsequent action in that thread
- 2.An unlock on a monitor happens-before every subsequent lock on that monitor.
- 3.A write to a volatile field happens-before every subsequent read of that volatile
- 4.A call to start() on a thread happens-before any actions in the started thread.
- 5.All actions in a thread happen-before any other thread successfully returns from a join() on
that thread. - 6.If an action a happens-before an action b, and b happens before an action c, then a happensbefore c
四巫糙、實現(xiàn) --> #lock
再往下挖一層,會發(fā)現(xiàn)volatile關鍵字颊乘,轉(zhuǎn)換成指令以后参淹,會有一個#lock前綴...原來以為會有相應的內(nèi)存屏障指令,說好的內(nèi)存屏障的那些呢乏悄?
后來參考了資料[11]以及其他一些文章以后才了解到浙值,任何帶有l(wèi)ock前綴的指令以及CPUID等指令都有內(nèi)存屏障的作用。
參考
- 1.從JVM并發(fā)看CPU內(nèi)存指令重排序
- 2.Java并發(fā):volatile內(nèi)存可見性和指令重排
- 3.內(nèi)存屏障
- 4.jmm cookbook
- 5.內(nèi)存屏障保證緩存一致性優(yōu)化
- 6.Java多線程里總線鎖定和緩存一致性的問題
- 7.MESI protocol
- 8.jsr133
- 9.淺論Lock 與X86 Cache 一致性
- 10.Java volatile 關鍵字底層實現(xiàn)原理解析
- 11.Does lock xchg have the same behavior as mfence?
- 12.從硬體觀點了解 memory barrier 的實作和效果