Chapter 1-8. Summary
Last updated
Last updated
順序一致性記憶體模型(Sequential Consistency Model, 以下簡稱SC model)是一個理論參考模型, JMM和處理器記憶體模型在設計時,通常會把SC model作為參考. JMM和處理器記憶體模型在設計時會對SC model做一些放鬆, 因為如果完全按照SC model來實作處理器和JMM, 那麼很多的處理器/編譯器最佳化都會被禁止, 這對執行效能會有非常大的影響.
根據對不同類型讀/寫操作組合的執行順序之放寬, 可以把常見處理器的記憶體模型劃分為以下幾種:
放寬程式中寫-讀操作的順序, 由此產生了total store ordering記憶體模型(簡稱TSO)
在前者的基礎上, 繼續放寬程式中寫-寫操作的順序, 由此產生了partial store order記憶體模型(簡稱為PSO)
在前面兩者的基礎上, 繼續放寬程式中讀-寫和讀-讀操作的順序, 藉此產生了relaxed memory order記憶體模型(簡稱為RMO)和PowerPC記憶體模型.
要注意的是, 這裡處理器對讀/寫操作的放寬, 是以兩個操作之間不存在資料相依性為前提的 (因為處理器要遵守as-if-serial語意, 處理器不會對存在資料相依性的兩個記憶體操作進行重排序).
以下表格顯示了常見處理器記憶體模型的細節特徵: 在這張表格中, 可以看到所有處理器記憶體模型都允許寫-讀重排序, 原因在第一章有提過: 因為它們都使用了write buffer, write buffer可能導致寫-讀操作重排序. 同時, 我們可以看到這些處理器記憶體模型都允許更早讀到當前處理器的寫入, 原因同樣是因為write buffer: 由於write buffer僅對當前處理器可見, 這個特性導致當前處理器可以比其它處理器早一步看到臨時保存在自己的write buffer中的寫入內容.
上面表格中的各種處理器記憶體模型, 從上到下, 模型由強至弱. 越是追求性能的處理器, 記憶體模型通常就設計的越弱. 因為這些處理器希望記憶體模型對其束縛越少越好, 這樣它們才可以做盡可能多的最佳化來提高性能. 由於常見的處理器記憶體模型比JMM要弱, Java編譯器在產生byte code時, 會在執行指令序列的適當位置插入記憶體屏障來限制處理器的重排序. 同時, 由於各種處理器記憶體模型的強弱並不相同, 為了在不同的處理器平台向開發者們展示一個一致的記憶體模型, JMM在不同的處理器中需要插入的記憶體屏障之數量和種類也不相同. 下圖是JMM在不同處理器記憶體模型中需要插入的記憶體屏障的示意圖: 如上圖所示, JMM屏蔽了不同處理器記憶體模型的差異, 其在不同的處理器平台之上為Java開發者呈現了一個一致的記憶體模型.
JMM是一個語言級別的記憶體模型, 處理器記憶體模型是硬體級別的記憶體模型, SC model則是一個理論參考模型. 下圖是語言記憶體模型、處理器記憶體模型與SC model的強弱對比示意圖:
從上圖我們可以看出: 常見的4種處理器記憶體模型比常用的3種語言記憶體模型要弱, 處理器記憶體模型和語言記憶體模型則又都比SC model要來得弱. 同處理器記憶體模型一樣, 越是追求效能的語言, 其記憶體模型就越弱.
從JMM設計者的角度來說, 在設計JMM時, 需要考慮兩個關鍵因素:
開發者對記憶體模型的使用: 開發人員希望記憶體模型易於理解, 易於開發. 故開發人員希望基於一個強記憶體模型來撰寫程式.
編譯器和處理器對記憶體模型的實現: 編譯器和處理器希望記憶體模型對它們的束縛越少越好, 這樣它們就可以做盡可能多的最佳化以利提高效能. 故編譯器和處理器希望實作一個弱記憶體模型.
由於這兩個因素互相矛盾, 所以JSR-133在設計JMM時的核心目標就是要找到一個好的平衡點: 一方面要為開發者提供足夠強的記憶體可見性保證; 另一方面, 對編譯器和處理器的限制要盡可能的放寬. 下面接者看JSR-133是怎麼實現這個目標的, 為了具體說明, 這邊就再看一次在前面的章節提到過的計算圓面積的範例程式:
上面計算圓面積的程式存在三個happens-before關係: 1. A happens-before B 2. B happens-before C 3. A happens-before C
由於A happens-before B, happens-before的定義會要求: A操作執行的結果要對B可見, 且A操作的執行順序排在B操作之前. 但是從程式語意的角度來說, 對A和B進行重排序既不會改變程式的執行結果, 又能提高程式的執行效能(允許這種重排序減少了對編譯器和處理器最佳化的束縛). 也就是說, 上面這3個happens-before關係中, 雖然2與3是必須要的, 但1是不必要的. 因此, JMM把happens-before要求禁止的重排序分為了以下兩種:
會改變程式執行結果的重排序
不會改變程式執行結果的重排序
JMM對這兩種不同性質的重排序, 採取了不同的策略:
對於會改變程式執行結果的重排序, JMM要求編譯器和處理器必須禁止這種重排序.
對於不會改變程式執行結果的重排序, JMM對編譯器和處理器不做要求(JMM允許這種重排序).
以下是JMM的設計示意圖: 從上圖可以看出兩點:
JMM向開發者提供的happens-before規則能滿足開發者的需求. JMM的happens-before規則不但簡單易懂, 而且也向開發者提供了足夠強的記憶體可見性保證(有些記憶體可見性保證其實並不一定真實存在, 譬如上面的A happens-before B).
JMM對編譯器和處理器的束縛已經盡可能的少了. 從上面的分析我們可以看出, JMM其實是在遵循一個很基本的原則: 只要不改變程式的執行結果(指的是單執行緒程式與正確同步的多執行緒程式), 編譯器和處理器就可以盡可能的做最佳化. 比方說, 若編譯器經過細緻的分析後, 認定一個lock只會被單個執行緒存取, 那個這個lock就可以被消除. 又或著是說, 若編譯器經過分析後, 認定一個volatile變數僅僅只會被單個執行緒存取, 那麼編譯器可以把這個volatile變數當作一個普通變數來對待. 這些最佳化既不會改變程式的執行結果, 又能提高程式的執行效率.
Java程式的記憶體可見性保證按程式類型可以分為下列三類:
單執行緒程式: 單執行緒程式不會出現記憶體可見性問題. 編譯器, runtime與處理器會共同確保單執行緒程式的執行結果與該程式在SC model中的執行結果相同.
正確同步的多執行緒程式: 正確同步的多執行緒程式的執行將具有順序一致性(程式的執行結果與該程式在SC model中的執行結果相同). 這正是JMM關注的重點, JMM通過限制編譯器和處理器的重排序來為開發者提供記憶體可見性保證.
未同步/未正確同步的多執行緒程式: JMM為它們提供了最小安全性保障---執行緒執行時讀取到的值, 要就是之前某個執行緒寫入的值, 不然就是預設值(0/null/false).
只要多執行緒程式是正確同步的, JMM保證該程式在任意的處理器平台上的執行結果, 與該程式在SC model中的執行結果一致.
JSR-133對JDK5之前的舊記憶體模型的修補主要有兩個:
增強volatile的記憶體語意: 舊記憶體模型允許volatile變數與普通變數重排序. JSR-133嚴格限制volatile變數與普通變數的重排序, 使volatile的寫-讀和lock的釋放-獲取具有相同的記憶體語意.
增強final的記憶體語意: 在舊的記憶體模型中, 多次讀取同一個final變數的值可能會不相同. 為此, JSR-133為final增加了兩個重排序規則. 所以現在的final具有了初始化安全性.
下圖表示這三類程式在JMM中與在SC model中的執行結果之異同: