首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 多執行緒入門指南(6):volatile 關鍵字的陷阱與正確使用姿勢

     編輯:Java 相關知識

volatile 能做什麼?不能做什麼?

  • ✅ 可見性:寫入 volatile 變數對所有執行緒立即可見(強制刷新至主記憶體)。
  • ✅ 禁止指令重排:編譯器/JVM 不會將 volatile 讀寫與前後指令重排(happens-before 語義)。
  • ❌ 不保證複合操作原子性:n++、n = n + 1、**讀取-修改-寫入** 任何多步驟操作**不是原子**的。
  • ❌ 不提供互斥鎖:無法保護臨界區多變數不變條件。

實證:volatile 計數器依然失敗

public class VolatileCounter extends Thread {
    public static volatile int n = 0; // volatile 修飾

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            n = n + 1; // 非原子:讀取 n → 加 1 → 寫回
            try { Thread.sleep(1); } catch (InterruptedException ignored) {}
        }
    }

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[100];
        for (int i = 0; i < threads.length; i++) threads[i] = new VolatileCounter();
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        System.out.println("n = " + n); // 預期 1000,實際常見 800~999
    }
}

原因:volatile 只保證單次讀/寫的可見性,n = n + 1 包含三個位元碼指令,執行緒切換發生在中間時,更新會丟失。

正確替代方案對比

方案程式碼原理效能適用場景
synchronized 方法static synchronized void inc() { n++; }鎖 Class 物件,進入互斥中簡單同步、低爭用
AtomicInteger (推薦)AtomicInteger n = new AtomicInteger(); n.incrementAndGet();CAS 無鎖硬體指令高高併發計數器、序號生成
LongAdder (Java 8+)LongAdder n = new LongAdder(); n.increment();分段計數器,極低爭用極高統計計數、高併發累加
volatile + CAS 迴圈手寫 UNSAFE.compareAndSwapInt同上,但複雜易錯高教學/特殊需求

volatile 真正適用的場景

1. 狀態旗標(單一寫入者、多讀取者)

public class WorkerThread extends Thread {
    private volatile boolean shutdown = false; // 只有主執行緒寫,工作執行緒讀

    public void requestShutdown() { shutdown = true; }

    @Override
    public void run() {
        while (!shutdown) { // 保證能看見最新值
            doWork();
        }
        cleanup();
    }
}

2. 雙重檢查鎖單例

public class Singleton {
    private static volatile Singleton instance; // 關鍵:禁止重排序

    public static Singleton getInstance() {
        if (instance == null) { // 第一次檢查(無鎖)
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次檢查(有鎖)
                    instance = new Singleton(); // 非原子:分配記憶體 → 建構子 → 指派參考
                }
            }
        }
        return instance;
    }
}

無 volatile 時,instance = new Singleton() 可能被重排為:分配記憶體 → 指派參考 → 執行建構子,導致其他執行緒看到「非 null 但未初始化完成」的物件。

3. 獨立觀察模式(發布-訂閱旗標)

public class EventSource {
    private volatile boolean eventOccurred = false;

    public void trigger() { eventOccurred = true; }

    public boolean checkAndClear() {
        if (eventOccurred) {
            eventOccurred = false; // 單一寫入者(消費者)
            return true;
        }
        return false;
    }
}

volatile 失效的經典模式

模式程式碼問題修正
計數器volatile int n; n++;複合操作非原子AtomicInteger
檢查後行動if (volatileFlag) doSomething();檢查與行動間有間隙synchronized 或 AtomicReference
多變數不變條件volatile int x, y; // 需 x > y無法原子更新兩者synchronized 保護兩者
陣列/集合元素volatile int[] arr; arr[0]++;volatile 只保護參考,不保護元素AtomicIntegerArray 或鎖

記憶體模型視角:happens-before 規則

寫入 volatile 變數  ----happens-before---->  讀取同一 volatile 變數
// 這意味著:寫入前的所有記憶體操作(包括非 volatile 變數),
// 對讀取該 volatile 變數的執行緒都是可見的。

這是 volatile 能作為「發布」機制的理論基礎(如雙重檢查鎖、物件安全發布)。

最佳實踐清單

  1. 預設不用 volatile:優先用 Atomic*、Concurrent*、synchronized。
  2. 只用於「單一變數、單一寫入者、狀態旗標」:關閉旗標、單例發布、簡單事件通知。
  3. 陣列/集合元素不可用 volatile:改用 AtomicIntegerArray、CopyOnWriteArrayList 或鎖。
  4. 複合操作必須加鎖或用原子類別:n++、check-then-act、多變數不變條件。
  5. 配合 final 使用:final volatile 無意義(final 已保證初始化可見性),但 volatile 參考指向的物件內部欄位若需可見性,該物件內部欄位也需 volatile 或 final。

一句話總結:volatile 是「輕量級可見性保證」,不是「輕量級鎖」;用在旗標發布、單例雙重檢查,計數器請直接用 AtomicInteger。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved