處理器記憶體模型
順序一緻性記憶體模型是一個理論參考模型,JMM和處理器記憶體模型在設計時通常會把順序一緻性記憶體模型作為參照。JMM和處理器記憶體模型在設計時會對順序一緻性模型做一些放松,因為如果完全按照順序一緻性模型來實作處理器和JMM,那麼很多的處理器和編譯器優化都要被禁止,這對執行性能将會有很大的影響。
根據對不同類型讀/寫操作組合的執行順序的放松,可以把常見處理器的記憶體模型劃分為下面幾種類型:
- 放松程式中寫-讀操作的順序,由此産生了total store ordering記憶體模型(簡稱為TSO)。
- 在前面1的基礎上,繼續放松程式中寫-寫操作的順序,由此産生了partial store order 記憶體模型(簡稱為PSO)。
- 在前面1和2的基礎上,繼續放松程式中讀-寫和讀-讀操作的順序,由此産生了relaxed memory order記憶體模型(簡稱為RMO)和PowerPC記憶體模型。
注意,這裡處理器對讀/寫操作的放松,是以兩個操作之間不存在資料依賴性為前提的(因為處理器要遵守as-if-serial語義,處理器不會對存在資料依賴性的兩個記憶體操作做重排序)。
下面的表格展示了常見處理器記憶體模型的細節特征:
-------------- -------------- ------------------- ------------------- ------------------------------ ------------------------------ ------------------------------
記憶體模型名稱 對應的處理器 Store-Load 重排序 Store-Store重排序 Load-Load 和Load-Store重排序 可以更早讀取到其它處理器的寫 可以更早讀取到目前處理器的寫
TSO sparc-TSOX64 Y Y
PSO sparc-PSO Y Y Y
RMO ia64 Y Y Y Y
PowerPC PowerPC Y Y Y Y Y
在這個表格中,我們可以看到所有處理器記憶體模型都允許寫-讀重排序,原因在第一章以說明過:它們都使用了寫緩存區,寫緩存區可能導緻寫-讀操作重排序。同時,我們可以看到這些處理器記憶體模型都允許更早讀到目前處理器的寫,原因同樣是因為寫緩存區:由于寫緩存區僅對目前處理器可見,這個特性導緻目前處理器可以比其他處理器先看到臨時儲存在自己的寫緩存區中的寫。
上面表格中的各種處理器記憶體模型,從上到下,模型由強變弱。越是追求性能的處理器,記憶體模型設計的會越弱。因為這些處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化來提高性能。
由于常見的處理器記憶體模型比JMM要弱,java編譯器在生成位元組碼時,會在執行指令序列的适當位置插入記憶體屏障來限制處理器的重排序。同時,由于各種處理器記憶體模型的強弱并不相同,為了在不同的處理器平台向程式員展示一個一緻的記憶體模型,JMM在不同的處理器中需要插入的記憶體屏障的數量和種類也不相同。下圖展示了JMM在不同處理器記憶體模型中需要插入的記憶體屏障的示意圖:

如上圖所示,JMM屏蔽了不同處理器記憶體模型的差異,它在不同的處理器平台之上為java程式員呈現了一個一緻的記憶體模型。
JMM,處理器記憶體模型與順序一緻性記憶體模型之間的關系
JMM是一個語言級的記憶體模型,處理器記憶體模型是硬體級的記憶體模型,順序一緻性記憶體模型是一個理論參考模型。下面是語言記憶體模型,處理器記憶體模型和順序一緻性記憶體模型的強弱對比示意圖:
從上圖我們可以看出:常見的4種處理器記憶體模型比常用的3中語言記憶體模型要弱,處理器記憶體模型和語言記憶體模型都比順序一緻性記憶體模型要弱。同處理器記憶體模型一樣,越是追求執行性能的語言,記憶體模型設計的會越弱。
JMM的設計
從JMM設計者的角度來說,在設計JMM時,需要考慮兩個關鍵因素:
- 程式員對記憶體模型的使用。程式員希望記憶體模型易于了解,易于程式設計。程式員希望基于一個強記憶體模型來編寫代碼。
- 編譯器和處理器對記憶體模型的實作。編譯器和處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化來提高性能。編譯器和處理器希望實作一個弱記憶體模型。
由于這兩個因素互相沖突,是以JSR-133專家組在設計JMM時的核心目标就是找到一個好的平衡點:一方面要為程式員提供足夠強的記憶體可見性保證;另一方面,對編譯器和處理器的限制要盡可能的放松。下面讓我們看看JSR-133是如何實作這一目标的。
為了具體說明,請看前面提到過的計算圓面積的示例代碼:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
上面計算圓的面積的示例代碼存在三個happens- before關系:
- A happens- before B;
- B happens- before C;
- 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其實是在遵循一個基本原則:隻要不改變程式的執行結果(指的是單線程程式和正确同步的多線程程式),編譯器和處理器怎麼優化都行。比如,如果編譯器經過細緻的分析後,認定一個鎖隻會被單個線程通路,那麼這個鎖可以被消除。再比如,如果編譯器經過細緻的分析後,認定一個volatile變量僅僅隻會被單個線程通路,那麼編譯器可以把這個volatile變量當作一個普通變量來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率。
JMM的記憶體可見性保證
Java程式的記憶體可見性保證按程式類型可以分為下列三類:
- 單線程程式。單線程程式不會出現記憶體可見性問題。編譯器,runtime和處理器會共同確定單線程程式的執行結果與該程式在順序一緻性模型中的執行結果相同。
- 正确同步的多線程程式。正确同步的多線程程式的執行将具有順序一緻性(程式的執行結果與該程式在順序一緻性記憶體模型中的執行結果相同)。這是JMM關注的重點,JMM通過限制編譯器和處理器的重排序來為程式員提供記憶體可見性保證。
- 未同步/未正确同步的多線程程式。JMM為它們提供了最小安全性保障:線程執行時讀取到的值,要麼是之前某個線程寫入的值,要麼是預設值(0,null,false)。
下圖展示了這三類程式在JMM中與在順序一緻性記憶體模型中的執行結果的異同:
隻要多線程程式是正确同步的,JMM保證該程式在任意的處理器平台上的執行結果,與該程式在順序一緻性記憶體模型中的執行結果一緻。
JSR-133對舊記憶體模型的修補
JSR-133對JDK5之前的舊記憶體模型的修補主要有兩個:
- 增強volatile的記憶體語義。舊記憶體模型允許volatile變量與普通變量重排序。JSR-133嚴格限制volatile變量與普通變量的重排序,使volatile的寫-讀和鎖的釋放-擷取具有相同的記憶體語義。
- 增強final的記憶體語義。在舊記憶體模型中,多次讀取同一個final變量的值可能會不相同。為此,JSR-133為final增加了兩個重排序規則。現在,final具有了初始化安全性。
參考文獻
- Computer Architecture: A Quantitative Approach, 4th Edition
- Shared memory consistency models: A tutorial
- Intel® Itanium® Architecture Software Developer’s Manual Volume 2: System Architecture
- Concurrent Programming on Windows
- JSR 133 (Java Memory Model) FAQ
- The JSR-133 Cookbook for Compiler Writers
- Java theory and practice: Fixing the Java Memory Model, Part 2
關于作者
程曉明,Java軟體工程師,國家認證的系統分析師、資訊項目管理師。專注于并發程式設計,個人郵箱:
[email protected]。