volatile 是 Java 並發編程的基石,保證共享變數在多線程間的**可見性**與**有序性** (禁止指令重排)。本文從 CPU 快取架構出發,經由 JMM (Java 記憶體模型)、匯編指令 lock 前綴、MESI 快取一致性協議,層層剖析 volatile 底層實現機制。
| 存取層級 | 典型延遲 (時鐘週期) | 容量 |
|---|---|---|
| L1 快取 | 1~2 | 32~64 KB/核心 |
| L2 快取 | 10~20 | 256 KB~1 MB/核心 |
| L3 快取 | 30~50 | 共享 8~32 MB |
| 主記憶體 (DRAM) | 100~300 | GB 級 |
CPU 核心不直接訪問主記憶體,而是透過快取子系統。快取行 為基本傳輸單位 (通常 64 位元組)。多核心下,各核心擁有私有 L1/L2,共享 L3,引發快取一致性問題。
read/load (主存→工作存) → use/assign (運算) → store/write (工作存→主存)。volatile 變數立即刷回主存;讀取強制從主存載入。其他線程下次讀取必見最新值。volatile 讀寫與前後操作的指令重排 (編譯器 + CPU 級)。實現「寫屏障」與「讀屏障」效果。不保證原子性:volatile int i; i++; 仍非原子 (讀-改-寫三步)。需配合 AtomicInteger 或 synchronized。
狀態 含義 可讀 可寫 獨佔
M (Modified) 已修改、髒數據、僅本核心有 ✅ ✅ ✅
E (Exclusive) 乾淨、僅本核心有 ✅ ✅ (升為 M) ✅
S (Shared) 乾淨、多核心共享 ✅ ❌ (需升為 E/M) ❌
I (Invalid) 無效/無數據 ❌ ❌ ❌
volatile 寫入觸發的 MESI 狀態機流轉
- 核心 0 執行
lock add,對應快取行狀態為 E 或 M (已獨佔)。
- 核心 0 廣播 Read-For-Ownership (RFO) 訊息。
- 核心 1/2/3 偵測到 RFO,將該快取行標記為 I (Invalid)。
- 核心 0 完成寫入,快取行變為 M,數據最終寫回主存/L3。
- 核心 1 後續讀取該變數,發現 I 狀態,發起 Read 請求,從主存/L3 獲取最新值 (狀態變 S 或 E)。
六、volatile 讀取端:無特殊指令,依賴快取一致性
讀取 volatile 變數編譯為普通 mov 指令。但 JMM 要求讀取前插入 LoadLoad + LoadStore 屏障 (編譯器層面禁止重排)。硬體層面,若快取行狀態 S/E/M 直接讀取;若 I 則觸發快取未命中流程,經 MESI 協議獲取最新值。**硬體快取一致性協議天然保證了讀取可見性**。
七、總結:volatile 實現全景圖
Java 代碼: volatile int x = 1;
│
▼
JMM 語義: 可見性 + 有序性 (無原子性)
│
▼
編譯器: 插入 StoreStore/StoreLoad/LoadLoad/LoadStore 屏障 (禁止重排)
│
▼
JIT (C2): 生成 lock add [rsp], 0 指令 (x86) / membar (SPARC) / dmb (ARM)
│
▼
CPU 執行: lock 前綴 -> 快取行鎖定 (RFO) -> MESI 廣播失效 -> 寫回主存/L3
│
▼
其他核心: 偵測失效 -> 後續讀取觸發快取未命中 -> 從主存/L3 獲取最新值
│
▼
結果: 所有線程可見最新寫入值,且操作順序符合程序順序 (happens-before)
八、最佳實務建議
- ✅ 適用場景:狀態旗標 (
volatile boolean shutdown)、單次發布 (volatile Config config)、獨立觀察 (不依賴當前值計算新值)。
- ❌ 不適用:
i++、複合條件檢查 (if (v > 0) v--)、需原子性的計數器 → 用 AtomicInteger / LongAdder / synchronized。
- 🔧
volatile + final 組合:final volatile 無意義 (final 已保證發布安全)。
- 🔧 長陣列/大物件:
volatile 僅保證引用可見,**不保證陣列元素/物件內部欄位可見** → 用 AtomicReferenceArray / VarHandle (Java 9+)。