0.引言
理解 JMM (Java Memory Model - JMM) 是掌握 Java 并發編程的關鍵,它定義了多線程環境下,線程如何與主內存以及彼此之間交互內存數據。
核心目標: JMM 旨在解決多線程編程中的三個核心問題:
- 原子性 (Atomicity): 一個操作是不可中斷的,要么全部執行成功,要么完全不執行。
- 可見性 (Visibility): 一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。
- 有序性 (Ordering): 程序執行的順序可能和代碼編寫的順序不一致(指令重排序),JMM 定義了在何種情況下這種重排序是被允許或禁止的。
1.第一層:淺顯易懂 - 為什么需要 JMM? (The Problem)
想象一下一個簡單的場景:
public class VisibilityProblem {private static boolean flag = false; // 共享變量private static int value = 0; // 共享變量public static void main(String[] args) {// 線程 Anew Thread(() -> {while (!flag) { // 循環檢查 flag// 空循環,等待 flag 變為 true}System.out.println("Value: " + value); // 打印 value}).start();// 線程 Bnew Thread(() -> {value = 42; // 步驟 1:設置 valueflag = true; // 步驟 2:設置 flag}).start();}
}
- 直覺期望: 線程 B 先設置
value = 42
,然后設置flag = true
。線程 A 看到flag
變為true
后跳出循環,打印出Value: 42
。 - 現實問題:
- 可見性問題: 線程 B 修改了
flag
和value
,但這些修改可能只存在于線程 B 的 CPU 緩存或寄存器中,沒有立即寫回主內存。線程 A 在自己的緩存中看到的flag
可能仍然是false
,導致它永遠無法跳出循環。即使跳出了循環,它看到的value
也可能是0
而不是42
。 - 有序性問題: 編譯器或處理器為了優化性能,可能會對指令進行重排序。線程 B 中的
flag = true
操作可能在value = 42
之前執行。如果此時線程 A 看到了flag
為true
而跳出循環,它看到的value
就可能是未初始化的0
。
- 可見性問題: 線程 B 修改了
JMM 就是為了解決這類在多線程環境下因緩存、指令重排序等優化帶來的不可預測行為而制定的規則。
2.第二層:核心概念 - JMM 的抽象模型 (The Abstraction)
JMM 定義了一個抽象的內存模型,它屏蔽了底層硬件的具體實現細節(如 CPU 緩存、緩存一致性協議),為 Java 程序員提供了一個統一的視圖:
- 主內存 (Main Memory):
- 存儲所有共享變量(實例字段、靜態字段、數組元素)的原始值。
- 是線程間共享的區域。
- 工作內存 (Working Memory / Local Memory):
- 每個線程都有自己的工作內存。
- 存儲該線程使用到的共享變量的副本。
- 線程對共享變量的所有操作(讀取、賦值等)都必須在工作內存中進行,不能直接讀寫主內存。
- 工作內存是 JMM 的一個抽象概念,它涵蓋了 CPU 寄存器、各級緩存、寫緩沖區等硬件優化。
內存交互操作 (8 種原子操作)
JMM 定義了 8 種原子操作來描述線程、工作內存和主內存之間的交互:
lock
(鎖定):作用于主內存變量,標識其為線程獨占狀態。unlock
(解鎖):作用于主內存變量,釋放鎖定狀態。read
(讀取):作用于主內存變量,將變量值從主內存傳輸到線程的工作內存。load
(載入):作用于工作內存變量,將read
操作得到的值放入工作內存的變量副本中。use
(使用):作用于工作內存變量,將變量值傳遞給執行引擎(如進行計算)。assign
(賦值):作用于工作內存變量,將執行引擎接收到的值賦給工作內存變量。store
(存儲):作用于工作內存變量,將變量值從工作內存傳輸到主內存。write
(寫入):作用于主內存變量,將store
操作傳輸過來的值放入主內存變量中。
規則: JMM 規定這些操作必須滿足特定的順序和約束(如 read
和 load
、store
和 write
必須成對按順序出現),但允許在成對操作之間插入其他操作(這是導致可見性和有序性問題的根源之一)。更關鍵的規則體現在 happens-before
原則上。
3.第三層:關鍵機制 - Happens-Before (HB)
happens-before
是 JMM 的核心概念,它定義了兩個操作之間的偏序關系。如果操作 A happens-before
操作 B,那么:
- 可見性保證: A 對共享變量的修改(結果)一定對 B 可見。
- 有序性保證: A 在程序順序上一定排在 B 之前執行(禁止某些重排序)。
注意: happens-before
并不一定意味著時間上的先后!它強調的是可見性和順序的保證。如果兩個操作之間沒有 happens-before
關系,JVM 可以隨意對它們進行重排序。
3.1JMM 定義的天然 Happens-Before 規則
- 程序次序規則 (Program Order Rule): 在同一個線程中,按照控制流順序(可能是分支、循環等),前面的操作
happens-before
后面的操作。 - 管程鎖定規則 (Monitor Lock Rule): 一個
unlock
操作happens-before
于后續對同一個鎖的lock
操作。 - volatile 變量規則 (Volatile Variable Rule): 對一個
volatile
變量的寫操作happens-before
于后續對這個變量的讀操作。 - 線程啟動規則 (Thread Start Rule):
Thread.start()
調用happens-before
于被啟動線程中的任何操作。 - 線程終止規則 (Thread Termination Rule): 線程中的所有操作都
happens-before
于其他線程檢測到該線程已經終止(如Thread.join()
返回成功或Thread.isAlive()
返回false
)。 - 線程中斷規則 (Thread Interruption Rule): 對線程
interrupt()
的調用happens-before
于被中斷線程檢測到中斷事件(拋出InterruptedException
或調用isInterrupted()
/interrupted()
)。 - 對象終結規則 (Finalizer Rule): 一個對象的初始化完成(構造函數執行結束)
happens-before
于它的finalize()
方法的開始。 - 傳遞性 (Transitivity): 如果 A
happens-before
B,且 Bhappens-before
C,那么 Ahappens-before
C。
happens-before
的意義: 程序員只需要利用這些規則(主要是通過 synchronized
、volatile
、final
等關鍵字以及 java.util.concurrent
包中的工具),就能確保多線程操作的可見性和有序性,無需關心底層復雜的緩存和重排序細節。
4.第四層:深入剖析 - volatile 關鍵字
volatile
是 JMM 中最重要的關鍵字之一,它提供了比 synchronized
更輕量級的同步機制。
4.1volatile 的語義
- 保證可見性:
- 對一個
volatile
變量的寫操作,會立即刷新到主內存。 - 對一個
volatile
變量的讀操作,會從主內存中讀取最新的值(或保證看到最近寫入的值)。 - 這通過禁止編譯器/處理器對
volatile
變量的讀寫操作進行緩存優化來實現。
- 對一個
- 禁止指令重排序 (部分):
- 編譯器在生成字節碼時,會在
volatile
寫操作前后插入寫屏障 (StoreStore + StoreLoad)。 - 在
volatile
讀操作前后插入讀屏障 (LoadLoad + LoadStore)。 - 寫屏障 (Store Barrier):
StoreStore
: 確保在該屏障之前的所有普通寫操作都刷新到主內存(對其他線程可見)。StoreLoad
: 確保在該屏障之前的volatile
寫操作都完成,并且刷新到主內存后,才能執行該屏障之后的volatile
讀/寫操作(開銷較大,通常由StoreLoad
承擔)。
- 讀屏障 (Load Barrier):
LoadLoad
: 確保在該屏障之后的所有讀操作(普通讀或volatile
讀)都在該屏障之后的讀操作之前執行(禁止重排序),并且強制從主內存或最新緩存中加載數據。LoadStore
: 確保在該屏障之后的所有寫操作都在該屏障之后的寫操作之前執行(禁止重排序),并且這些寫操作的數據依賴在該屏障之前的讀操作已完成。
- 這些屏障共同作用,確保了
volatile
變量讀寫操作相對于其前后代碼的相對順序,從而實現了happens-before
規則中的有序性保證。
- 編譯器在生成字節碼時,會在
4.2volatile 的局限性
- 不保證原子性:
volatile
不能保證復合操作的原子性。例如volatile int count; count++;
這個操作 (count++
包含讀-改-寫三步) 在多線程下仍然是不安全的。需要使用synchronized
或AtomicInteger
等。
4.3volatile 的典型用法
- 狀態標志位: 如開頭的例子,用
volatile boolean flag;
來安全地通知其他線程狀態改變。 - 一次性安全發布 (One-Time Safe Publication): 利用
volatile
寫操作的StoreStore
屏障,確保在發布對象引用之前,對象的初始化已經完全完成(構造函數結束)。
如果沒有public class Singleton {private static volatile Singleton instance; // volatile 保證安全發布private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查 (無鎖)synchronized (Singleton.class) {if (instance == null) { // 第二次檢查 (加鎖)instance = new Singleton(); // volatile 寫}}}return instance;} }
volatile
,其他線程可能看到一個未初始化完成的Singleton
對象(指令重排序導致引用賦值在構造函數完成之前發生)。 - 獨立觀察 (Independent Observation): 定期發布觀察結果供其他程序使用。
- 開銷較低的讀-寫鎖策略: 當讀遠多于寫時,可以用
volatile
保證寫操作的可見性,讀操作不需要加鎖(但寫操作需要額外的同步機制如synchronized
或 CAS 來保證原子性)。
5.第五層:JMM 與并發編程實踐
理解 JMM 對于編寫正確、高效、可預測的并發程序至關重要:
- 優先使用高級并發工具:
java.util.concurrent
包 (ConcurrentHashMap
,ExecutorService
,CountDownLatch
,CyclicBarrier
,AtomicXxx
類等) 是構建在 JMM 基礎之上的,它們通常比直接使用synchronized
和volatile
更安全、更高效、更易用。理解 JMM 能讓你更好地理解和使用這些工具。 - 正確使用 synchronized:
synchronized
塊不僅提供互斥(原子性),也提供強大的內存語義:進入synchronized
塊(獲得鎖)相當于執行一個volatile
讀操作(能看到之前持有該鎖的線程的所有修改),退出synchronized
塊(釋放鎖)相當于執行一個volatile
寫操作(將修改刷新到主內存)。這確保了臨界區內外操作的可見性和有序性。 - 理解 final 字段: 被
final
修飾的字段在構造函數中初始化后,其值對其他線程是可見的(無需同步),前提是對象的引用本身是正確發布的(例如通過volatile
或synchronized
安全發布規則)。這是 JMM 對final
的特殊保證。 - 避免過度同步: 不必要的同步會帶來性能開銷。理解 JMM 可以幫助你判斷何時需要同步(主要是為了保護共享可變狀態),何時可以避免。
- 警惕內存可見性導致的微妙 Bug: 很多并發 Bug 不是由競態條件引起的,而是由內存可見性問題引起的。理解 JMM 是診斷這類 Bug 的基礎。
6.總結
- JMM 是什么? 一個規范,定義了多線程環境下 Java 程序如何與內存交互,確保程序在并發執行時的可見性、有序性(以及通過其他機制如鎖保證的原子性)。
- 核心問題: 解決因 CPU 緩存、指令重排序導致的可見性和有序性問題。
- 抽象模型: 主內存(共享)和工作內存(線程私有副本),定義了 8 種內存交互操作。
- 核心機制:
happens-before
關系。它定義了操作間的可見性和順序保證。JMM 定義了一系列天然規則(程序次序、鎖、volatile
、線程啟動/終止等)。 volatile
關鍵字:- 保證可見性(寫立即刷新,讀獲取最新值)。
- 通過內存屏障(
StoreStore
,StoreLoad
,LoadLoad
,LoadStore
)禁止特定類型的指令重排序。 - 不保證原子性。
- 實踐意義: 理解 JMM 是編寫正確、高效并發 Java 程序的基礎。它解釋了高級并發工具的工作原理,指導你正確使用
synchronized
、volatile
、final
,并幫助你診斷復雜的并發 Bug。
掌握 JMM 需要時間和實踐。從理解基本問題和抽象模型開始,逐步深入到 happens-before
和 volatile
的細節,最終將其應用于并發編程實踐中,是學習 JMM 的有效路徑。