volatile關鍵字?MESI協議?指令重排?記憶體屏障?這都是啥玩意
一、摘要
三級快取,MESI快取一致性協議,指令重排,記憶體屏障,JMM,volatile。單拿一個出來,想必大家對這些概念應該有一定了解。但是這些東西有什麼必然的聯絡,或者他們之間究竟有什麼前世今生想必是困擾大家的一個問題。
為什麼有了MESI協議,我們還需要volatile?
記憶體屏障的由來?
指令重排帶來的問題?
下面我們通過分析每一個技術的由來,以及帶來的負面影響,跟大家探討一下這些技術之間的聯絡。具體每個關鍵詞相關文章也很多不再贅述,只談個人理解。
二、三級快取篇
1,三級快取的由來
CPU的發展很快,幾乎18-24個月,CPU的計算能力就能翻一番,然而記憶體的發展卻相對緩慢,以至於記憶體的讀寫速度遠遠跟不上CPU的計算能力,也就是說,在CPU和記憶體的資源交換中,CPU常常需要等待記憶體,而浪費了大量的計算能力。三級快取的出現正是為了彌補記憶體的慢和CPU的快而誕生的產物。
所謂的三級快取正是誕生在這樣的一個背景下。
三級快取分為三種:L1 Cache,L2 Cache,L3 Cache
記憶體以及三級快取的響應時間如上所示,由此可知記憶體和快取的差距還是很大的,當時硬體價格和上圖響應時間是成正比的。響應時間越快,價格越貴。
2,CPU快取架構
不同的CPU廠商的架構也有些不同,在這裡只介紹流行的快取架構
快取的意義
1 時間侷限性:如果某個資料被訪問,那麼它在不久的將來有可能被在次訪問 2 空間侷限性:如果某個快取行的資料被訪問,那麼與之相鄰的快取行很快可能被訪問到
3,快取帶來的問題
快取的加入是為了解決CPU運算能力和記憶體讀寫能力的不匹配問題,簡單來說就是為了提升資源利用率。那麼在多CPU(一個CPU對應一個或者多個核心)或者多核心下,每個核心都會有一個一級快取或者二級快取,也就是說一二級快取是核心獨佔的(類似JMM模型,執行緒的工作記憶體是執行緒獨佔的,主記憶體是共享的)而三級快取和主記憶體是共享的,這樣就將導致CPU快取一致性問題。為了解決這種不一致,大名鼎鼎的MESI協議隨之而來。
三、MESI快取一致性協議篇
1,MESI協議
MESI協議的由來上個章節已經介紹,在此不做過多介紹
MESI:Modified(修改),Exclusive(獨佔),Shared(共享),Invalid(無效)由以上資料的四種狀態的首字母而來。
在快取中資料的儲存單元是快取行(Cache line),主流的CPU快取行都是64個位元組。快取行的四種狀態由兩個位元組標識。
M(Modified) |
這行資料有效,資料被修改了,和記憶體中的資料不一致,資料只存在於本Cache中。 |
E(Exclusive) |
這行資料有效,資料和記憶體中的資料一致,資料只存在於本 Cache 中。 |
S(Shared) |
這行資料有效,資料和記憶體中的資料一致,資料存在於很多 Cache 中。 |
I(Invalid) |
這行資料無效。 |
當一個快取行的狀態調整,另外一個快取行需要調整的狀態如下:
M |
E |
S |
I |
|
M |
× |
× |
× |
√ |
E |
× |
× |
× |
√ |
S |
× |
× |
√ |
√ |
I |
√ |
√ |
√ |
√ |
模擬快取行獨佔(E)過程:
核心core1發出載入資料指令,通過bus從記憶體中載入了一個數據A進入到該核心的快取行,並且發現該資料是沒有被別的核心載入的,此時該核心將會將該資料A狀態置為獨佔即E狀態。
模擬快取行共享(S)過程:
上面core1已經獨佔了資料A,此時core2也發指令要讀取資料A,此時發現該資料已經被core1佔有,然後通知core1“我也要使用該資料”,所以core1將資料A狀態改為S即 E >>> S。
模擬快取行修改(M)過程:
假如有三個核心:core 1,core 2分別快取了資料A。因為多個核心都快取了該資料,即在各個核心中該資料的狀態都是Shared。
此時core 1需要修改資料A。
core 1:核心計算完成,通過指令寫入快取行,資料A的狀態將從S >>> M
core 2:core1將通知快取了該資料的核心“我修改了該資料”,所以core2會將該快取置為無效。因為資料已經發生了變化,core2快取的資料A將不再有任何意義。
埋個伏筆,以上過程提到的“通知”的過程可能是很耗時的,在此期間core1將會處於等待迴應。浪費了core1的計算能力。
2,MESI協議帶來的問題
多個core的快取狀態置換是需要消耗時間的,導致核心在此期間將無事可做。甚至一旦某一個核心發生阻塞,將會導致其他核心也處於阻塞,從而帶來效能和穩定性的極大消耗。
所以這時指令重排開始發揮它的價值。想想這種等待有時是沒有必要的,因為在這個等待時間內核心完全可以去幹一些其他事情。即當核心處於等待狀態時,不等待當前指令結束接著去處理下一個指令。
四、處理器指令重排篇
前面介紹了指令重排將會減少處理器的等待時間進而去處理其他的指令。這種一個指令還未結束便去執行其它指令的行為稱之為,指令重排。大白話就是指令未按需執行。
1,指令重排的實現
Store Bufferes---儲存快取
store buffer即儲存快取。位於核心和快取之間。當處理器需要處理將計算結果寫入在快取中處於shared狀態的資料時,需要通知其他核心將該快取置為 Invalid(無效),引入store buffer後將不再需要處理器去等待其他核心的響應結果,只需要把修改的資料寫到store buffer,通知其他核心,然後當前核心即可去執行其它指令。當收到其他核心的響應結果後,再把store buffer中的資料寫回快取,並修改狀態為M。(很類似分散式中,資料一致性保障的非同步確認)
Invalidate Queues---失效佇列
簡單說處理器修改資料時,需要通知其它核心將該快取中的資料置為Invalid(失效),我們將該資料放到了Store Bufferes處理。那收到失效指令的這些核心會立即處理這種失效訊息嗎?答案是不會的,因為就算是一個核心快取了該資料並不意味著馬上要用,這些核心會將失效通知放到Invalidate Queues,然後快速返回Invalidate Acknowledge訊息(意思就是儘量不耽誤正在用這個資料的核心正常工作)。後續收到失效通知的核心將會從該queues中逐個處理該命令。(意思就是我也不著急用,所以我也不著急處理)。
2,指令重排帶來的問題---可見性問題
指令重排或者說store buffer或者invalidte queues帶來的問題就是可見性問題,通過以上分析,我們不難發現這其實是保障了資料的最終一致性。因為在處理器對資料的修改不是立即對其他核心可見的,因為修改了的資料被放在了store buffer中,通知其他核心的資料修改也不是達到其他核心並被立即處理的。其實有點非同步處理的意思。
資料不一致性問題
假設core1對資料A的修改通知沒有被core2立即處理(因為在invalidte queues中),core2緊接著又修改了資料A,是不是就造成了資料的不一致。其它核心對資料的修改對本核心是不可見的。
為了提升效能進入了指令重排,而然指令重排可能會導致資料的不一致,這可如何是好?哈哈哈,當然是有解決方案的,答案就是記憶體屏障。更多精彩內容且聽下回分解。
五、記憶體屏障
1,記憶體屏障
上一章節中說到指令重排導致的可見性問題可能會導致資料的不一致。CPU就給我們提供了一直通過軟體告知CPU什麼指令不能重排,什麼指令能重排的機制就是記憶體屏障。
兩個指令:
load:將記憶體中的資料拷貝到核心的快取中。 store:將核心快取的資料重新整理到記憶體中。
記憶體屏障:Memory Barrier。
記憶體屏障又分為四種:
LoadLoad Barriers(讀屏障),StoreStore Barriers(寫屏障),LoadStore Barriers,StoreLoad Barriers
不同的CPU架構對記憶體屏障的實現是不盡相同的,我們這裡討論流行的X86架構。
X86中有三種記憶體屏障:
Store Memory Barrier:寫屏障,等同於前文的StoreStore Barriers
告訴處理器在執行這之後的指令之前,執行所有已經在儲存快取(store buffer)中的修改(M)指令。即:所有store barrier之前的修改(M)指令都是對之後的指令可見。
Load Memory Barrier:讀屏障,等同於前文的LoadLoad Barriers
告訴處理器在執行任何的載入前,執行所有已經在失效佇列(Invalidte Queues)中的失效(I)指令。即:所有load barrier之前的store指令對之後(本核心和其他核心)的指令都是可見的。
Full Barrier:萬能屏障,即Full barrier作用等同於以上二者之和。
即所有store barrier之前的store指令對之後的指令都是可見的,之後(本核心和其他核心)的指令也都是可見的,完全保證了資料的強一致性。
2,記憶體屏障的問題
CPU知道什麼時候需要加入記憶體屏障,什麼時候不需要嗎?CPU將這個加入記憶體屏障的時機交給了程式設計師。在java中這個加入記憶體屏障的命令就是volatile關鍵字。
澄清一點,volatile並不是僅僅加入記憶體屏障這麼簡單,加入記憶體屏障只是volatile核心指令級別的記憶體語義。
除此之外:volatile還可以禁止編譯器的指令重排,因為JVM為了優化效能並且不違反happens-before原則的前提下也會進行指令重排。
六、Volatile對可見性和有序性的保障
併發下的三個概念:
原子性(Atomicity):一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗。 可見性(Visibility):所有執行緒都能看到共享記憶體的最新狀態。 有序性(Ordering):即程式執行的順序按照程式碼的先後順序執行。
Volatile是JMM(Java Memory Model:java記憶體模型)中對可見性和有序性的保障。
Volatile記憶體語義:
可見性:可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。 有序性:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。其實就是禁止指令重排。
說了這麼多終於說到了Volatile的有序性,Volatile正是通過對處理器加入記憶體屏障來禁止指令重排從來保證有序性的。大白話來說Volatile就是程式設計師決定加入記憶體屏障的指令。
當然在java語言中,指令重排並不是只發生在處理器層面。在java編譯器中JVM也會存在指令重排優化,以提高程式的效能。Volatile同樣可以禁止編譯器層面的指令重排。
下面這段話摘自《深入理解Java虛擬機器》:
“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令”
lock字首指令實際上相當於前文介紹的插入記憶體屏障指令。(這個lock指令是JMM中的,再往底層就是加入記憶體屏障指令)
關於JMM,happens-before原則及volatile的其它內容後面打算再寫一篇介紹。
七、總結
- CPU與三級快取:為了解決CPU按照摩爾定律提升的計算能力和記憶體緩慢發展的不平衡,三級快取以其比記憶體更加強悍的讀寫能力,在CPU和記憶體中間充當了一層快取,緩解了這種發展不平衡帶來的矛盾。
- 三級快取與MESI協議:各種CPU架構的核心都有各自的一級快取或者二級快取,而三級快取多是多核共享,核心之間的資料共享將導致三級快取間的快取一致性問題,從而有了MESI CPU快取一致性協議來保證資料的一致性問題。
- MESI協議:MESI協議本質上是多核操作共享資料的序列化強一致性保障,這種序列將導致CPU的資源的浪費。
- 指令重排優化與最終一致性:在多核間Invalidate訊息的同步響應浪費了CPU一定的計算資源,從而有了指令重排。在核心處理器中對資料的修改不直接寫到快取中,而是先放入store buffer,然後向其他核心傳送Invalidate訊息的期間,本核心可以繼續執行其他指令。而收到Invalidate訊息的核心將Invalidate訊息放到Invalidate Queues中延時處理。當本核心收到其他核心關於Invalidate的invalidate acknowledge響應才會將store buffer中的對應指令重新整理到快取中。這樣store buffer和Invalidate Queues實現的指令重排就通過非同步確認和延時消費(這是分散式的概念,但是我覺在用在這裡很合理)保證了資料的最終一致性。
- 記憶體屏障與強一致性:如上所說指令重排是採用的是非同步確認和延時消費保障了資料的最終一致性,但是在有些場景下這種機制也將導致資料的不一致,所以在一些特殊場景下我們需要禁止指令重排從而保證資料的強一致性。禁止指令重排的機制就是對CPU加入記憶體屏障,從而禁止一些會引起錯誤的指令重排。
- 在CPU角度,他是無法判斷什麼時候需要加入記憶體屏障,什麼時候不需要(假如核心自己去判斷,相信這個判斷將會比保障強一致性更浪費資源,那指令重排就畫蛇添足了),所以這個加入記憶體屏障的活就拋給了上層應用也就是軟體層。在Java語言中,如果完全將加入記憶體屏障的活交給程式設計師,相信Java程式設計師會非常痛苦。所以JMM的一些規定比如happens-before和一些可見性和有序性的規則,將會減少程式設計師的壓力,因為在一些JVM可預測的場景中,JMM規範會自動實現這種記憶體屏障。而程式設計師需要關心的就是在一些高併發場景下我們可以使用Volatile關鍵字去告訴JVM和CPU我要加入記憶體屏障。當然這不是Volatile存在的所有意義。
最後想說一句,想徹底理解Volatile不瞭解以上那些知識是不夠的。
感謝螢幕前的你能看到最後。
右下角推薦,謝謝。
如有錯誤的地方還請留言指正。
原創不易,轉載請註明原文地址:https://www.cnblogs.com/hello-shf/p/12091591.html
參考文獻:
https://blog.51cto.com/14220760/2370118?source=dra
https://www.zhihu.com/question/296949412
https://www.open-open.com/lib/view/open1426815960648.h