爲何會有內存屏障?java
爲了提高數據加載速度有了CPU緩存,在多核狀況下帶來了緩存一致性問題,能夠經過MESI緩存一致性協議來解決。但MESI緩存一致性協議下,一個CPU可能須要等待另外一個CPU響應後才能繼續執行,致使了阻塞,影響性能(能夠參考以前voltile那篇文章,由於涉及到總線加鎖或者緩存鎖定)。因此增長了StoreBuffer和InvalidateQueue,也就是須要store時先放到StoreBuffer裏,而後繼續執行下一條指令,等到其餘CPU響應返回後再處理對應store;收到invalidate通知時也不當即處理,而是先放到InvalidateQueue,並當即給予對方響應,而後等到合適時機再一塊兒處理。這種優化提高了CPU執行能力,但也使得MESI協議的操做沒法當即獲得處理,產生了可見性和有序性問題。編程
什麼是內存屏障?緩存
它是一個CPU指令。它是這樣一條指令:安全
a)確保一些特定操做執行的順序。併發
b)影響一些數據的可見性(多是某些指令執行後的結果)。性能
有序性:編譯器和CPU能夠在保證輸出結果同樣的狀況下對指令重排序,使性能獲得優化。插入一個內存屏障,至關於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。優化
可見性:內存屏障另外一個做用是強制更新一次不一樣CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將獲得最新值,而不用考慮究竟是被哪一個cpu核心或者哪顆CPU執行的。線程
內存屏障的種類設計
LoadLoad 屏障3d
序列:Load1,Loadload,Load2
確保Load1所要讀入的數據可以在被Load2和後續的load指令訪問前讀入。一般能執行預加載指令或/和支持亂序處理的處理器中須要顯式聲明Loadload屏障,由於在這些處理器中正在等待的加載指令可以繞過正在等待存儲的指令。 而對於老是能保證處理順序的處理器上,設置該屏障至關於無操做。
StoreStore 屏障
序列:Store1,StoreStore,Store2
確保Store1的數據在Store2以及後續Store指令操做相關數據以前對其它處理器可見(例如向主存刷新數據)。一般狀況下,若是處理器不能保證從寫緩衝或/和緩存向其它處理器和主存中按順序刷新數據,那麼它須要使用StoreStore屏障。
LoadStore 屏障
序列: Load1; LoadStore; Store2
確保Load1的數據在Store2和後續Store指令被刷新以前讀取。在等待Store指令能夠越過loads指令的亂序處理器上須要使用LoadStore屏障。
StoreLoad 屏障
序列: Store1; StoreLoad; Load2
確保Store1的數據在被Load2和後續的Load指令讀取以前對其餘處理器可見。StoreLoad屏障能夠防止一個後續的load指令 不正確的使用了Store1的數據,而不是另外一個處理器在相同內存位置寫入一個新數據。正由於如此,因此在下面所討論的處理器爲了在屏障前讀取一樣內存位置存過的數據,必須使用一個StoreLoad屏障將存儲指令和後續的加載指令分開。Storeload屏障在幾乎全部的現代多處理器中都須要使用,但一般它的開銷也是最昂貴的。它們昂貴的部分緣由是它們必須關閉一般的略過緩存直接從寫緩衝區讀取數據的機制。這可能經過讓一個緩衝區進行充分刷新(flush),以及其餘延遲的方式來實現。
volatile語義中的內存屏障
volatile的內存屏障策略很是嚴格保守,很是悲觀且毫無安全感的心態:
在每一個volatile寫操做前插入StoreStore屏障,在寫操做後插入StoreLoad屏障;
在每一個volatile讀操做前插入LoadLoad屏障,在讀操做後插入LoadStore屏障;
因爲內存屏障的做用,避免了volatile變量和其它指令重排序、線程之間實現了通訊,使得volatile表現出了鎖的特性。
final語義中的內存屏障
對於final域,編譯器和CPU會遵循兩個排序規則:
新建對象過程當中,構造體中對final域的初始化寫入和這個對象賦值給其餘引用變量,這兩個操做不能重排序;(廢話嘛)
初次讀包含final域的對象引用和讀取這個final域,這兩個操做不能重排序;(晦澀,意思就是先賦值引用,再調用final值)
總之上面規則的意思能夠這樣理解,必需保證一個對象的全部final域被寫入完畢後才能引用和讀取。這也是內存屏障的起的做用:
寫final域:在編譯器寫final域完畢,構造體結束以前,會插入一個StoreStore屏障,保證前面的對final寫入對其餘線程/CPU可見,並阻止重排序。
讀final域:在上述規則2中,兩步操做不能重排序的機理就是在讀final域前插入了LoadLoad屏障。
X86處理器中,因爲CPU不會對寫-寫操做進行重排序,因此StoreStore屏障會被省略;而X86也不會對邏輯上有前後依賴關係的操做進行重排序,因此LoadLoad也會變省略。
對性能的影響
內存屏障做爲另外一個CPU級的指令,沒有鎖那樣大的開銷。內核並無在多個線程間干涉和調度。但凡事都是有代價的。內存屏障的確是有開銷的——編譯器/cpu不能重排序指令,致使不能夠儘量地高效利用CPU,另外刷新緩存亦會有開銷。因此不要覺得用volatile代替鎖操做就一點事都沒。
你會注意到Disruptor的實現對序列號的讀寫頻率儘可能降到最低。對volatile字段的每次讀或寫都是相對高成本的操做。可是,也應該認識到在批量的狀況下能夠得到很好的表現。若是你知道不該對序列號頻繁讀寫,那麼很合理的想到,先得到一整批Entries,並在更新序列號前處理它們。這個技巧對生產者和消費者都適用。如下的例子來自BatchConsumer:
long nextSequence = sequence + 1; while (running) { try { final long availableSequence = consumerBarrier.waitFor(nextSequence); while (nextSequence <= availableSequence) { entry = consumerBarrier.getEntry(nextSequence); handler.onAvailable(entry); nextSequence++; } handler.onEndOfBatch(); sequence = entry.getSequence(); } catch (final Exception ex) { exceptionHandler.handle(ex, entry); sequence = entry.getSequence(); nextSequence = entry.getSequence() + 1; } }
在上面的代碼中,咱們在消費者處理entries的循環中用一個局部變量(nextSequence)來遞增。這代表咱們想盡量地減小對volatile類型的序列號的進行讀寫。
總結
內存屏障是CPU指令,它容許你對數據何時對其餘進程可見做出假設。在Java裏,你使用volatile關鍵字來實現內存屏障。使用volatile意味着你不用被迫選擇加鎖,而且還能讓你得到性能的提高。
可是,你須要對你的設計進行一些更細緻的思考,特別是你對volatile字段的使用有多頻繁,以及對它們的讀寫有多頻繁。
參考資料
【併發編程概述:內存屏障、volatile、原子變量和互斥鎖】https://qilu.me/2019/03/16/2019-03-16/
【內存屏障】https://www.jianshu.com/p/2ab5e3d7e510
【剖析Disruptor:爲何會這麼快?(三)揭祕內存屏障】http://ifeve.com/disruptor-memory-barrier/
【volatile與內存屏障總結】https://zhuanlan.zhihu.com/p/43526907