volatile 是 Java 並發編程中輕量級的同步機制,主打可見性與禁止指令重排序。許多開發者知道這個關鍵字,卻難以在實際程式中觀察到它的效果。本文透過一個經典的「可見性失效」案例,配合 Python 壓力測試腳本,讓你親眼見證 volatile 的關鍵作用。
public class NoVisibility {
private boolean ready = false;
private int num = 0;
private class ReaderThread extends Thread {
@Override
public void run() {
int count = 0;
while (!ready) { // 這裡會被 JIT 優化成無限循環讀取暫存器值
count++;
}
System.out.println("num = " + num); // 永遠執行不到這裡
}
}
public static void main(String[] args) throws InterruptedException {
NoVisibility test = new NoVisibility();
test.new ReaderThread().start();
Thread.sleep(100); // 確保讀執行緒先跑起來
test.num = 43;
test.ready = true; // 寫入 ready = true
System.out.println("main thread end");
}
}
因為 JVM 即時編譯(JIT)優化時機不固定,單次執行可能無法複現。我們用 Python 反覆啟動 JVM 來提高複現機率:
import os
i = 0
while True:
os.system("java -server NoVisibility") # -server 觸發 Server VM 深度優化
i += 1
print("run count: " + str(i))
while (!ready) { count++; } 迴圈體極簡單,JIT 极易將 ready 讀取優化為暫存器快取,導致主執行緒寫入 ready = true 對讀執行緒不可見。System.out.println(),因其內部含同步塊,會強制刷新記憶體屏障,意外讓 ready 變可見——這只是副作用,非正解。private volatile boolean ready = false; // 加上 volatile
volatile 變數時,JMM 會插入 StoreStore + StoreLoad 屏障,強制將快取刷新到主記憶體;讀取時插入 LoadLoad + LoadStore 屏障,強制從主記憶體讀取最新值。volatile 讀寫與其前後的普通讀寫重排序,保證 num = 43 必定在 ready = true 之前對其他執行緒可見。volatile 適合「一寫多讀」、「狀態標誌」等場景。若涉及複合操作(如 count++),仍需 synchronized 或 AtomicInteger。下次遇到多執行緒狀態不同步,先檢查有無漏加 volatile。