最近又看了下Disruptor,里面提到了內(nèi)存屏障,,突然想到了指令重排,、還有可見(jiàn)性,感覺(jué)里面關(guān)系有點(diǎn)亂,,就翻了下,,因此就寫(xiě)了這篇文章 帶著幾個(gè)問(wèn)題:
一、可見(jiàn)性和MESI1.1 可見(jiàn)性在JVM的內(nèi)存模型中,,每個(gè)線程有自己的工作內(nèi)存,,實(shí)際上JAVA線程借助了底層操作系統(tǒng)線程實(shí)現(xiàn),一個(gè)JVM線程對(duì)應(yīng)一個(gè)操作系統(tǒng)線程,,線程的工作內(nèi)存其實(shí)是cpu寄存器和高速緩存的抽象 現(xiàn)代處理器的緩存一般分為三級(jí),,由每一個(gè)核心獨(dú)享的L1、L2 Cache,,以及所有的核心共享L3 Cache組成,,具體每個(gè)cache,實(shí)際上是有很多緩存行組成: 1.2 緩存一致性和MESI緩存一致性協(xié)議給緩存行(通常為64字節(jié))定義了個(gè)狀態(tài):獨(dú)占(exclusive),、共享(share),、修改(modified)、失效(invalid),,用來(lái)描述該緩存行是否被多處理器共享、是否修改,。所以緩存一致性協(xié)議也稱MESI協(xié)議,。
協(xié)議協(xié)作如下:
這個(gè)圖的含義就是當(dāng)一個(gè)core持有一個(gè)cacheline的狀態(tài)為Y時(shí),其它c(diǎn)ore對(duì)應(yīng)的cacheline應(yīng)該處于狀態(tài)X, 比如地址 0x00010000 對(duì)應(yīng)的cacheline在core0上為狀態(tài)M, 則其它所有的core對(duì)應(yīng)于0x00010000的cacheline都必須為I , 0x00010000 對(duì)應(yīng)的cacheline在core0上為狀態(tài)S, 則其它所有的core對(duì)應(yīng)于0x00010000的cacheline 可以是S或者I , 另外MESI協(xié)議為了提高性能,,引入了Store Buffe和Invalidate Queues,,還是有可能會(huì)引起緩存不一致,還會(huì)再引入內(nèi)存屏障來(lái)確保一致性,,可以參考[7]和[12] 存儲(chǔ)緩存(Store Buffe)也就是常說(shuō)的寫(xiě)緩存,,當(dāng)處理器修改緩存時(shí),,把新值放到存儲(chǔ)緩存中,處理器就可以去干別的事了,,把剩下的事交給存儲(chǔ)緩存,。 失效隊(duì)列(Invalidate Queues)處理失效的緩存也不是簡(jiǎn)單的,需要讀取主存,。并且存儲(chǔ)緩存也不是無(wú)限大的,,那么當(dāng)存儲(chǔ)緩存滿的時(shí)候,處理器還是要等待失效響應(yīng)的,。為了解決上面兩個(gè)問(wèn)題,,引進(jìn)了失效隊(duì)列(invalidate queue)。處理失效的工作如下:
1.3 MESI和CAS關(guān)系在x86架構(gòu)上,,CAS被翻譯為”lock cmpxchg...“,當(dāng)兩個(gè)core同時(shí)執(zhí)行針對(duì)同一地址的CAS指令時(shí),其實(shí)他們是在試圖修改每個(gè)core自己持有的Cache line,
對(duì)于我們的CAS操作來(lái)說(shuō), 其實(shí)鎖并沒(méi)有消失,只是轉(zhuǎn)嫁到了ring bus的總線仲裁協(xié)議中. 而且大量的多核同時(shí)針對(duì)一個(gè)地址的CAS操作會(huì)引起反復(fù)的互相invalidate 同一cacheline, 造成pingpong效應(yīng), 同樣會(huì)降低性能(參考[9]),。當(dāng)然如果真的有性能問(wèn)題,,我覺(jué)得這可能會(huì)在ns級(jí)別體現(xiàn)了,一般的應(yīng)用程序中使用CAS應(yīng)該不會(huì)引起性能問(wèn)題 二、指令重排和內(nèi)存屏障2.1 指令重排現(xiàn)代CPU的速度越來(lái)越快,,為了充分的利用CPU,,在編譯器和CPU執(zhí)行期,都可能對(duì)指令重排,。舉個(gè)例子: LDR R1, [R0];//操作1 ADD R2, R1, R1;//操作2 ADD R3, R4, R4;//操作3 上面這段代碼,,如果操作1如果發(fā)生cache miss,則需要等待讀取內(nèi)存外存,??纯从袥](méi)有能優(yōu)先執(zhí)行的指令,操作2依賴于操作1,,不能被優(yōu)先執(zhí)行,操作3不依賴1和2,,所以能優(yōu)先執(zhí)行操作3,。 在并發(fā)模型下,,重排序還是可能會(huì)引發(fā)問(wèn)題,,比較經(jīng)典的就是“單例模式失效”問(wèn)題(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; } } 上面這段代碼,初看沒(méi)問(wèn)題,,但是在并發(fā)模型下,,可能會(huì)出錯(cuò),那是因?yàn)閕nstance= new Singleton()并非一個(gè)原子操作,它實(shí)際上下面這三個(gè)操作: memory =allocate(); //1:分配對(duì)象的內(nèi)存空間 ctorInstance(memory); //2:初始化對(duì)象 instance =memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址 上面操作2依賴于操作1,,但是操作3并不依賴于操作2,,所以JVM是可以針對(duì)它們進(jìn)行指令的優(yōu)化重排序的,經(jīng)過(guò)重排序后如下: memory =allocate(); //1:分配對(duì)象的內(nèi)存空間 instance =memory; //3:instance指向剛分配的內(nèi)存地址,,此時(shí)對(duì)象還未初始化 ctorInstance(memory); //2:初始化對(duì)象 可以看到指令重排之后,,instance指向分配好的內(nèi)存放在了前面,而這段內(nèi)存的初始化被排在了后面,。在多線程場(chǎng)景下,,可能A線程執(zhí)行到了3,B線程發(fā)現(xiàn)已經(jīng)不為空就返回繼續(xù)執(zhí)行,,就會(huì)出錯(cuò),。 在java里面volatile可以防止重排,當(dāng)然還有另外一個(gè)作用即內(nèi)存可見(jiàn)性,,這個(gè)知道的人還應(yīng)該比較普遍,,就不說(shuō)了 2.2 內(nèi)存屏障硬件層的內(nèi)存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫(xiě)屏障。內(nèi)存屏障有兩個(gè)作用:
在JSR規(guī)范中定義了4種內(nèi)存屏障:
對(duì)于volatile關(guān)鍵字,,按照規(guī)范會(huì)有下面的操作:
具體到X86來(lái)看,其實(shí)沒(méi)那么多指令,,只有StoreLoad: 結(jié)合上面的【一】和【二】的內(nèi)容,,內(nèi)存屏障首先阻止了指令的重排,另外也和MESI協(xié)議結(jié)合,,確保了內(nèi)存的可見(jiàn)性 三,、happends-before結(jié)合前面的兩點(diǎn),再看happends-before就比較好理解了,。因?yàn)楣庹f(shuō)可見(jiàn)性和重排很難聯(lián)想到happends-before,。這個(gè)點(diǎn)在并發(fā)編程里還是非常重要的,再詳細(xì)記錄下:
四,、實(shí)現(xiàn) --> #lock再往下挖一層,,會(huì)發(fā)現(xiàn)volatile關(guān)鍵字,轉(zhuǎn)換成指令以后,,會(huì)有一個(gè)#lock前綴...原來(lái)以為會(huì)有相應(yīng)的內(nèi)存屏障指令,,說(shuō)好的內(nèi)存屏障的那些呢? 參考
|
|
來(lái)自: liang1234_ > 《鎖機(jī)制》