Java Memory Model

Java Memory Model(JMM)

本篇文章說明Java memory model(JMM)為什麼會存在,其是為了解決什麼問題而產生的,以及其如何克服問題。

Why

在多核CPU的普遍現在,相同一段代碼在實作語言層面使用多執行緒時,可能會執行在不同的CPU核心中,這會帶來以下兩個問題

  1. 快取一致性(Cache coherence)問題
  2. CPU優化和指令重排

快取一致性(Cache coherence)問題

每個CPU核心都含有L1 cache,在多執行緒場景下,每個CPU核心對共享數據就可能存在不一致的情況,例如運算完的數據沒有即時同步回主記憶體中,導致在另一個CPU核心進行的操作,是基於上一次操作結果的數據內容。

CPU優化和指令重排

為了提高效率與並行度,現代CPU會在執行之前,對傳入的程式進行優化,就是會將傳入的指令進行亂序執行處理。另外Java虛擬機的JIT也會執行指令重排。
對程式進行重排序時,會遵循as-if-serial語意,其保證重排序的結果不能改變單執行緒下的執行結果,和CPU、編譯器和Runtime都必須遵循as-if-serial語意。
在什麼情況下,重排序會對程式的執行結果會有影響呢?就是在上下文語句中包含數據依賴關係的操作。換句話說,若在單執行緒的環境下,若上下文代碼不存在數據依賴關係,則可以進行重排序。

原子性(Atomicity)、可見性(Visibility)、有序性(Ordering)

  • 原子性
    • 一個操作在CPU不能被中斷,只能整個操作完成,或整個操作失敗並回滾。
  • 可見性
    • 多執行緒同時訪問一個相同變量時,當一個執行緒修改了變量,其他執行緒可以立即看到變更後的值。
  • 有序性
    • 程式執行的順序按照代碼的先後順序執行

綜合以上談到的,快取一致性問題即是可見性問題;而CPU優化會導致原子性問題,CPU優化與指令重排會導致有序性問題。

How

針對上述在多執行緒可能導致非預期結果的潛在問題,CPU廠商提出了對應的優化限制方法-記憶體屏障(Memory Barrier)。
記憶體屏障提供2個功能:

  1. 保證數據的可見性
  2. 防止指令之間的重排序

保證數據的可見性

保證某些資料或者某條指令的執行結果的記憶體可見性,在插入Memory Barrier後,將會在將在Barrier之前寫入cache的資料,刷回主記憶體中,從而保證任何CPU上的執行緒都可以讀到最新版本得資料。解決快取一致性問題

防止指令之間的重排序

保證CPU或編譯器在記憶體進行特定操作時,嚴格按照一定的順序執行,在插入Memory Barrier時,CPU與編譯器會得知,不能將Memory Barrier前後的指令進行重排。解決在多執行緒場景下,CPU或編譯器重排序帶來的潛在問題。

Java memory model(JMM)

JMM的作用是提供語句,封裝管理CPU與編譯器的優化,讓開發者可以在併發環境下,讓程式的表現跟預期的一樣。
JMM透過Happens-before關係向開發者提供跨執行緒的記憶體可見性

  • JR133保證併發性一致性
    • 可見性
      • 保證共享變量的可見性
    • 原子性
      • 保證讀/寫原子性
    • 有序性
      • 使用鎖保證 一些行為一定在之前
      • 禁止重排序

JR133提出新的JAVA的記憶體模型,其在設計時,考慮到程式開發者、編譯器、CPU之間的關係;開發者希望在一個強約束且易於理解的強記憶體模型基礎上進行開發;在CPU與編譯器的角度,希望記憶體模型不要有過多的干預,讓它們可以盡可能的優化來提高性能。
故JR133提出了Happens-Before,在兩者間取得適當的平衡,另外提出volatile, final的語法改進,讓開發者能在多執行緒場景下,保證程式的執行結果。

Happen-before

JMM規定了JVM必須遵循一組最小保證,這組保證規定了對變量的寫入操作在何時將對其他執行緒可見。
JMM為程式中所有的操作定義了一個偏序關係,稱之為Happens-Before。
Happens-Before的處理對象有兩者:

  1. 重排序後與Happens-Before關係指定順序的執行結果不同
  2. 重排序後與Happens-Before關係指定順序的執行結果相同
    (不影響執行結果是指單執行緒以及正確同步的多執行緒程式)

JMM面對上述兩者不同性質的重排序,有著不同的策略

  1. 會改變執行結果的重排序,JMM要求CPU/編譯器/JVM禁止對程式進行重排序
  2. 不會改變執行結果的重排序,JMM對CPU/編譯器/JVM的重排序不做干預

若想保證執行操作B的執行緒看到操作A的結果(無論是否在相同執行緒當中),那麼在A與B之間必須滿足Happens-Before的關係,如果兩個操作之間缺乏Happens-Before的關係,那麼CPU/編譯器/JVM可以對它們任意的重排序;例如未正確同步的多執行緒操作。
如果在讀操作和寫操作之間沒有依照Happens-Before來排序,那麼就會產生數據競爭問題,在正確同步的程式中不存在數據競爭,並且會表現出串型一致性,這意味程式中的所有操作都會按照一種固定的和全局的順序執行。
雖然Happens-Before只滿足偏序關係,但同步操作,如鎖的獲得與釋放等操作、volatile變量的讀取與寫入操作,都滿足全序關係,因此,在描述Happens-Before關係時,就可以使用”後續的鎖獲取操作”,和”後續的volatile變量讀取操作”等表達術語來達成全序關係。
由上述的策略中,JMM可以在開發者與CPU、編譯器的兩者間做出較好的權衡,一方面保證的易懂與記憶體可見性,一方面保證足夠的優化空間。

Volatile, Final的語法改進

  • Volatile
    • JR133嚴格限制volatile變量與普通變量的重排序,使得volatile的寫-讀和鎖的釋放具有相同的記憶體語意
  • Final
    • JR133為final增加兩個重排序功能,讓final具有初始化安全性

Summury

近年來,CPU透過不斷優化來提升效能,包含重排序、時鐘頻率、提升的併發度度和多層快取,編譯器也在不斷改進,通過對指令重新排序來實現優化,以及使用成熟的全局寄存器分配算法。
在共享記憶體的多處理器體系架構中,每個處理器都擁有自己的快取,並且定期與主記憶體進行協調,在不同的處理器架構中提供了不同級別的快取一致性,其中一部分只提供最小的保證,作業系統、編譯器以及運行時(有時甚至包含應用程式)需要彌合在硬件能力與執行緒安全需求之間的差異。想要確保每個處理器都能在任意時刻知道其他處理器正在進行的工作,將需要非常大的開銷,在大多時候,這些信息是不需要的,因此處理器會適當放寬存儲一致性,以換取效能的提升。
在處理器架構定義的記憶體模型中將告訴應用程式可以獲得怎樣的保證,此外還定義了一些特殊的指令(稱為記憶體屏障),當需要共享數據時,這些指令就能實現額外的存儲協調保證。
Java提供了JMM,讓Java開發人員無需關心不同架構上記憶體模型之間的差異,並且JVM通過在適當的位置上插入記憶體屏障來屏蔽在JMM與底層平台記憶體模型之間的差異。
為了保證原子性、可見性和有序性,JMM定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範,透過這些規範對記憶體的讀寫操作,進而保證指令執行的正確性,其規範包含CPU優化、多層快取、優化、指令重排序等。
Java關鍵字對應在併發場景下提供的保證:

  • 原子性
    • Java提供兩個高級字節碼指令monitorenter和monitorexit,因此在Java中可以使用synchronized保證方法和代碼塊內的操作是原子性的
  • 可見性
    • volatile提供在多執行緒環境下的可見性保證,被其修飾的變量在被修改後,可以立即同步到主記憶體中,被其修飾的變量在每次使用之前都從主記憶體中取得
  • 有序性
    • synchronized和volatile提供在多執行緒之間操作的有序性,其可以透過Happen-before關係提供保證

Reference

  • Java并发编程实战(中文版)
  • 深入理解Java内存模型