承接基石篇,本文深入剖析「單線程看起來順序執行」的 as-if-serial 語義如何與多線程可見性產生張力,通過經典 Flag 例子演示重排序如何破壞多線程邏輯,並解析控制依賴與猜測執行的隱患。
若兩操作訪問同一變量,且至少一寫,則存在數據依賴:
| 類型 | 代碼模式 | 重排序後果 |
|---|---|---|
| 寫後讀 (RAW) | a = 1; b = a; | b 讀到舊值,邏輯錯 |
| 寫後寫 (WAW) | a = 1; a = 2; | 最終值變為 1,邏輯錯 |
| 讀後寫 (WAR) | a = b; b = 1; | a 讀到新值 1,邏輯錯 |
鐵律:編譯器/CPU 絕不 重排序有數據依賴的操作(單線程內)。這是 as-if-serial 語義的微觀基礎。
定義:不管如何重排序,單線程程序的執行結果不能被改變。
編譯器、Runtime、CPU 共同遵守。因此:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
依賴圖:A→C、B→C。A、B 無依賴,可重排為 B→A→C 或 A→B→C,結果不變。C 必須在 A、B 之後。
HB 規則:線程內前操作 hb 後操作。但 hb 不要求物理先執行,只要求「結果可見且邏輯順序」。
上例中 A hb B,但 CPU 可先執行 B 再執行 A。因 A 結果不需對 B 可見,且重排後最終結果一致,JMM 判定合法。
核心洞察:JMM 允許「物理執行順序 ≠ 邏輯順序」,只要單線程語義保持。這為硬件並行度騰出空間。
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() { // 線程 A
a = 1; // 1
flag = true; // 2
}
public void reader() { // 線程 B
if (flag) { // 3
int i = a * a; // 4
}
}
}
CPU/編譯器將 2 移到 1 前:
A: flag = true (2)
B: if (flag) → true
B: i = a * a // 此時 a 仍為 0!邏輯崩潰
A: a = 1 (1)
Flag 本意是「a 已就緒」的信號,重排導致信號提前發射,數據未就緒。
3 是 4 的控制條件。CPU 猜測執行:預執行 4 (a*a) 存入 ROB (Reorder Buffer),3 判真時提交結果。
A: a = 1; flag = true;
B: 推測執行 i = a*a (a 可能仍為 0)
B: if (flag) → true
B: 提交推測結果 i=0
結論:單線程中控制依賴重排安全(as-if-serial),多線程中極其危險。
| 維度 | 單線程 | 多線程 |
|---|---|---|
| 語義保證 | as-if-serial:結果不變 | 需額外同步建立 HB 關係 |
| 控制依賴重排 | 安全(推測執行不改結果) | 不安全(其他線程可見中間態) |
| 數據依賴 | 硬件自動保護 | 跨線程不受保護,需 volatile/synchronized |
// 方案 1:volatile 禁止 1↔2 和 3↔4 重排
volatile boolean flag;
// 方案 2:synchronized 建立監視器鎖 HB
synchronized(lock) { a = 1; flag = true; }
synchronized(lock) { if (flag) i = a*a; }
// 方案 3:原子類/並發包
AtomicInteger a = new AtomicInteger(0);
AtomicBoolean flag = new AtomicBoolean(false);
// 仍需注意操作原子性組合問題
重排序不是 Bug,是硬件/編譯器為性能合法優化。多線程程序員的職責,是通過 JMM 提供的同步原語,在需要的地方「拉住」順序,構建正確的 HB 圖。