在 Java 多線程併發編程中,volatile 關鍵字常被誤以為是解決所有線程安全問題的「銀彈」。許多開發者認為只要給共享變量加上 volatile,就能在併發環境下高枕無憂。然而現實並非如此——volatile 僅保證可見性與有序性,並不保證原子性。本文將通過實例與底層原理分析,釐清這一關鍵字的真實能力邊界。
Java 提供了兩種核心同步手段:
以下代碼啟動 1000 個線程對共享計數器執行 count++:
public class Counter {
public static volatile int count = 0;
public static void inc() {
try { Thread.sleep(1); } catch (InterruptedException e) {}
count++; // 非原子操作:讀取 → 修改 → 寫回
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(Counter::inc).start();
}
// 稍作等待確保線程結束
try { Thread.sleep(2000); } catch (InterruptedException e) {}
System.out.println("運行結果: Counter.count = " + Counter.count); // 典型輸出:992、995 等,而非 1000
}
}
即便加上 volatile,結果依然小於 1000。根因在於 count++ 包含三步:read → modify → write,多線程交替執行時會導致更新丟失。
根據 Java 內存模型(JMM),每個線程擁有私有工作內存,共享變量存儲於主內存。線程操作變量的流程為:
關鍵點:上述步驟非原子。線程 A 讀取值 5 後,線程 B 也讀取 5;A 修改為 6 寫回主內存,B 仍基於舊值 5 計算並寫回 6,導致一次遞增丟失。
volatile 變量時,JMM 會強制將工作內存值刷回主內存;讀取時會強制從主內存加載最新值,使其他線程立即可見。volatile 讀寫指令進行重排序(內存屏障)。i++、check-then-act 等複合操作,volatile 無法防止競態條件。| 場景 | 是否適用 volatile | 建議方案 |
|---|---|---|
狀態標誌位(如 boolean running) | ✅ 是 | 直接使用 |
| 單次寫入、多次讀取的配置值 | ✅ 是 | 直接使用 |
| 計數器、累加器 | ❌ 否 | AtomicInteger、LongAdder 或 synchronized |
複合條件檢查(如 if (x > 0) x--) | ❌ 否 | synchronized 或鎖 |
volatile 是 Java 併發編程中的「輕量級利器」,適用於單次寫入、即時可見的場景。但面對讀-改-寫等非原子複合操作時,必須轉向 java.util.concurrent.atomic 包或顯式鎖機制。理解其語義邊界,才能寫出正確且高效的併發代碼。