記憶體屏障(Memory barrier)
一、為什麼會有記憶體屏障
- 每個CPU都會有自己的緩存(有的甚至L1,L2,L3),緩存的目的就是為了提高性能,避免每次都要向記憶體取。但是這樣的弊端也很明顯:不能實時的和記憶體發生資訊交換,分在不同CPU執行的不同線程對同一個變量的緩存值不同。
- 用volatile關鍵字修飾變量可以解決上述問題,那麼volatile是如何做到這一點的呢?那就是記憶體屏障,記憶體屏障是硬體層的概念,不同的硬體平台實作記憶體屏障的手段并不是一樣,java通過屏蔽這些差異,統一由jvm來生成記憶體屏障的指令。
二、記憶體屏障是什麼
- 硬體層的記憶體屏障分為兩種:
和Load Barrier
即讀屏障和寫屏障。Store Barrier
- 記憶體屏障有兩個作用:
- 阻止屏障兩側的指令重排序;
- 強制把寫緩沖區/高速緩存中的髒資料等寫回主記憶體,讓緩存中相應的緩存行的資料失效。
- 對于Load Barrier來說,在指令前插入Load Barrier,可以讓高速緩存中的資料失效,強制從新從主記憶體加載資料;
- 對于Store Barrier來說,在指令後插入Store Barrier,能讓寫入緩存中的最新資料更新寫入主記憶體,讓其他線程可見。
三、volatile如何保證可見性、防止指令重排序
volatile保持記憶體可見性和防止指令重排序的原理,本質上是同一個問題,也都依靠記憶體屏障得到解決
在x86處理器下通過工具擷取JIT編譯器生成的彙編指令來看看對Volatile進行寫操作CPU會做什麼事情。
Java代碼: instance = new Singleton();//instance是volatile變量
彙編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);
lock字首指令相當于一個記憶體屏障(也稱記憶體栅欄),記憶體屏障主要提供幾個功能:
1、 確定指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;
2、 如果對聲明了Volatile變量進行寫操作,JVM就會向處理器發送一條Lock字首的指令,将這個變量所在緩存行的資料寫回到系統記憶體。但是就算寫回到記憶體,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,是以在多處理器下,為了保證各個處理器的緩存是一緻的,就會實作緩存一緻性協定MESI,每個處理器通過嗅探在總線上傳播的資料來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的記憶體位址被修改,就會将目前處理器的緩存行設定成無效狀态,當處理器(包括其他處理器)要對這個資料進行操作的時候,會強制重新從主記憶體裡把資料讀到該處理器緩存裡。
通俗的講:
對聲明了Volatile變量進行寫操作,把目前線程工作記憶體中的變更(可以了解為寫緩沖區)都寫到主記憶體中,并且使得其他cpu關于這部分資料的緩存行也都失效(MESI機制)。也就是說,unlock之後,其他線程使用使用這部分共享變量,由于MESI導緻相關緩存行失效,那麼讀取這部分資料就會重新從主記憶體中讀取資料。
可參考:https://www.zhihu.com/question/41016480/answer/130906913
關于緩存行是什麼:https://blog.csdn.net/qq_36951116/article/details/109253507
四、java記憶體屏障
- java的記憶體屏障通常所謂的四種即
,LoadLoad
,StoreStore
,LoadStore
實際上也是上述兩種的組合,完成一系列的屏障和資料同步功能。StoreLoad
- LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被通路前,保證Load1要讀取的資料被讀取完畢。
- StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
- LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
- StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實作中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能
五、volatile語義中的記憶體屏障
- volatile的記憶體屏障政策非常嚴格保守,非常悲觀且毫無安全感的心态:
(1)在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;
即保證在後面的讀寫操作之前把資料刷回主記憶體,保證刷回的資料對其他處理器可見。
(2)在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;
即保證在後面的讀寫操作之前把要讀取的資料被讀取完畢。
- 由于記憶體屏障的作用,避免了volatile變量和其它指令重排序、線程之間實作了通信,使得volatile表現出了鎖的特性。
如下例子解釋如何放置屏障:
class X {
int a, b;
volatile int v, u;
void f() {
int i, j;
i = a;// load a,加載全局變量a
j = b;// load b,加載全局變量b
// LoadLoad
i = v;// load v,加載全局變量v,由于是volatile變量,是以在前後需要加屏障
// LoadStore
// LoadLoad
j = u;// load u,加載全局變量u,由于是volatile變量,是以在前後需要加屏障
// LoadStore
a = i;// store a
b = j;// store b
// StoreStore
v = i;// store v,儲存全局變量v,由于是volatile變量,是以在前後需要加屏障
// StoreLoad
// StoreStore
u = j;// store u,儲存全局變量u,由于是volatile變量,是以在前後需要加屏障
// StoreLoad
// LoadLoad
i = u;// load u,加載全局變量u,由于是volatile變量,是以在前後需要加屏障
// LoadStore
j = b;// load b
a = i;// store a
}
}
六、final語義中的記憶體屏障
- 對于final域,編譯器和CPU會遵循兩個排序規則:
- 建立對象過程中,構造體中對final域的初始化寫入和這個對象指派給其他引用變量,這兩個操作不能重排序;(廢話嘛)
- 初次讀包含final域的對象引用和讀取這個final域,這兩個操作不能重排序;(晦澀,意思就是先指派引用,再調用final值)
- 總之上面規則的意思可以這樣了解,必需保證一個對象的所有final域被寫入完畢後才能引用和讀取。這也是記憶體屏障的起的作用:
- 寫final域:在編譯器寫final域完畢,構造體結束之前,會插入一個StoreStore屏障,保證前面的對final寫入對其他線程/CPU可見,并阻止重排序。
- 讀final域:在上述規則2中,兩步操作不能重排序的機理就是在讀final域前插入了LoadLoad屏障。
- X86處理器中,由于CPU不會對寫-寫操作進行重排序,是以StoreStore屏障會被省略;而X86也不會對邏輯上有先後依賴關系的操作進行重排序,是以LoadLoad也會變省略。
參考:
https://blog.csdn.net/breakout_alex/article/details/94379895
https://www.jianshu.com/p/2ab5e3d7e510
http://ifeve.com/jmm-cookbook-mb/