Java內存模型(JMM)的設計是建立在物理機的內存模型之上的,因此了解物理機內存模型的原理也十分重要。簡單來說,物理機的內存模型經歷了3個階段:
- 早期的CPU計算速率與內存操作速率相當,CPU直接從內存中讀取數據,運行程序計算,得出結果再寫入內存;
- 后來CPU飛速發展,內存的速率已經遠不及CPU的計算了,這時CPU計算任務因等待內存數據讀取而停滯,造成計算資源浪費,於是人們設計了緩存,CPU通過讀寫緩存來獲取操作數,結果數據也通過緩存寫入內存。緩存的讀寫速度盡量接近CPU,在一定程度上緩解了CPU計算資源浪費的情況。CPU越來越快時,緩存的速度也相應提高,人們就通過設計多級緩存作為CPU與內存之間的緩沖,如現在的二級緩存,三級緩存。
- CPU發展到達瓶頸,單個核的計算頻率已經很難提高了。人們為了獲得更高的處理效率開始引入多核CPU。在多核CPU中,每個核擁有自己的緩存。可能會出現兩個核的緩存里都擁有相同數據的副本,在兩個核獨自進行修改的情況下導致數據的不一致。因此需要解決緩存的一致性問題。
1、問題的根源
緩存中存儲數據,緩存不一致就意味着相同的數據在不同的緩存中呈現着不同的表現。對於存儲的數據,CPU有讀操作和寫操作,讀操作不會影響數據的存儲狀態,寫操作是導致不一致的根源。但是緩存不一致的問題並不是因為我們采用了多核CPU,而是因為我們采用了多個緩存。如果多核CPU共享一個緩存,那么不一致問題也不復存在,在每個時鍾周期,幾個核通過某種方式競爭使用緩存,每個時刻只允許一個核對緩存進行讀寫操作,其他的核都需排隊等候,這樣緩存永遠是一致的,但是這種方式會導致CPU計算資源的極大浪費,同時效率極低。采用每個核一個緩存的方式,多核可以同時工作,但是也帶來了緩存不一致的問題。
因此問題的根源不在於多個核,而是多個緩存,以及緩存的寫操作。
2、緩存一致性協議
為了解決緩存不一致的問題,我們需要一種機制來約束各個核,也就是緩存一致性協議。
我們常用的緩存一致性協議都是屬於“snooping(窺探)”協議,各個核能夠時刻監控自己和其他核的狀態,從而統一管理協調。窺探的思想是:CPU的各個緩存是獨立的,但是內存卻是共享的,所有緩存的數據最終都通過總線寫入同一個內存,因此CPU各個核都能“看見”總線,即各個緩存不僅在進行內存數據交換的時候訪問總線,還可以時刻“窺探”總線,監控其他緩存在干什么。因此當一個緩存在往內存中寫數據時,其他緩存也都能“窺探”到,從而按照一致性協議保證緩存間的同步。
3、MESI協議
MESI協議是一種常用的緩存一致性協議,它通過定義一個狀態機來保證緩存的一致性。在MESI協議中有四種狀態,這些狀態都是針對緩存行(緩存由多個緩存行組成,緩存行的大小單位與機器的位數相關)。
- (I)Invalid狀態:緩存行無效狀態。要么該緩存行數據已經過時,要么緩存行數據已經不在緩存中。對於無效狀態,可直接認為緩存行未加載進緩存。
- (S)Shared狀態:緩存行共享狀態。緩存行數據與內存中對應數據保持一致,多個緩存中的相應緩存行都是共享狀態。該狀態下的緩存行只允許讀取,不允許寫。
- (E)Exclusive狀態:緩存行獨有狀態。該緩存行中的數據與內存中對應數據保持一致,當某緩存行是獨有狀態,其他緩存對應的緩存行都必須為無效狀態。
- (M)Modified狀態:緩存行已修改狀態。緩存行中的數據為臟數據,與內存中的對應數據不一致。如果一個緩存行為已修改狀態,那么其他緩存中對應緩存行都必須為無效狀態。另外,如果該狀態下的緩存行狀態被修改為無效,那么臟段必須先回寫入內存中。
MESI協議的定律:所有M狀態下的緩存行(臟數據)回寫后,任意緩存級別中的緩存行的數據都與內存保持一致。另外,如果某個緩存行處於E狀態,那么在其他的緩存中就不會存在該緩存行。
MESI協議保證了緩存的強一致性,在原理上提供了完整的順序一致性。可以說在MESI協議實現的內存模型下,緩存是絕對一致的,但是這也會導致一些效率的問題,我們平時使用的機器往往都不會采用這種強內存模型,而是在這個基礎上去使用較為弱一些的內存模型:如允許CPU讀寫指令的重排序等。這些弱內存模型可以帶來一定的效率提升,但是也引入了一些語義上的問題。
4、內存模型
前面講說MESI協議保證了緩存的強一致性,但是其實在這個基礎上還需要對CPU提出兩點要求:
- CPU緩存要及時響應總線事件
- CPU嚴格按照程序順序執行內存操作指令
只要保證了以上兩點,緩存一致性就能得到絕對的保證。但是由於效率的原因,CPU不可能保證以上兩點:
- 首先,總線事件到來之際,緩存可能正在執行其他的指令,例如向CPU傳輸數據,那么緩存就無法馬上響應總線事件了
- 其次,CPU如果嚴格按照程序順序執行內存操作指令,意味着修改數據之前,必須要等到所有其他緩存的失效確認(Invalidate Acknowledge),這個等待的過程嚴重影響CPU的計算效率,因此現代CPU大都采用存儲緩沖(Store Buffer)來暫時緩存寫入的數據,等所有的失效確認完成之后,再向內存中回寫數據。正是因為使用了存儲緩沖,導致一些數據的內存寫入操作可能會晚於程序中的順序,也就是重排序(reorder)。
- 另外,CPU的存儲緩沖大小是有限制的,有一些數據的回寫還是需要等待其他緩存的失效確認,而且失效操作本身也是比較耗時的,於是引入了失效隊列(invalidation queue)的概念
- 對於到來的失效請求,失效確認消息必須馬上發出;
- 發出消息后,失效操作放入失效隊列,並不馬上執行;
- 對於正在處理的緩存,CPU不給它發送任何消息
由於引入了存儲緩沖和失效隊列的概念,CPU的指令執行順序就更加混亂,讀操作有可能會讀取到過時的數據(失效操作還在失效隊列中),寫操作完成的時間可能比程序中的時間要晚(寫操作的數據在存儲緩沖中)。對應於內存模型就分成了兩個陣營:弱內存模型和強內存模型。弱內存模型的體系架構中,上述重排序優化的情況不能保證完全一致性,需要用戶代碼去保證,這樣用戶代碼會比較復雜一些,CPU的執行效率也就更高。而在強內存模型的體系架構中則相反,CPU負責實現復雜的操作來保證一致性,用戶代碼簡單但是執行效率低。
那么在弱內存模型下的用戶代碼如何保證CPU上述重排序動作不會導致一致性的問題呢:內存屏障(memory barriers):
- 寫屏障(store barrier):在執行屏障之后的指令之前,先執行所有已經在存儲緩沖中保存的指令。
- 讀屏障(load barrier):在執行任何的加載指令之前,先應執行所有已經在失效隊列中的指令。
有了內存屏障,就可以保證緩存的一致性了。這里所說的都是物理架構中的緩存情況,對於並發編程中的JMM,編譯器在生成字節碼的時候會插入特定類型的內存屏障來禁止重排序, 保證多線程下的內存可見性,具體在JMM中是如何實現的,下次再分析。