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

深度解析 Java volatile 關鍵字:記憶體語義、happens-before 與實戰應用

     編輯:Java 相關知識

volatile 的兩大核心語義

當共享變量(成員變量、靜態變量)被 volatile 修飾後,具備兩層關鍵語義:

  1. 可見性:一個線程修改的值,對其他線程立即可見
  2. 禁止指令重排序:確保讀寫操作的有序性

經典問題:為什麼需要 volatile?

// 線程 1
boolean stop = false;
while (!stop) {
    doSomething();
}

// 線程 2
stop = true;  // 嘗試停止線程 1

問題:線程 1 可能將 stop 複製到工作記憶體(CPU 快取),線程 2 修改主記憶體後,線程 1 看不到變化,導致無限循環。

volatile 如何解決可見性問題

  1. 寫入 volatile 變量時,強制將修改值立即刷新到主記憶體
  2. 寫入時,會導致其他線程該變量的快取行失效
  3. 其他線程讀取時,發現快取失效,重新從主記憶體讀取最新值

volatile 與鎖的等價性

對 volatile 變量的單次讀/寫,等同於使用同一把監視器鎖同步:

// volatile 版本
class VolatileExample {
    volatile long vl = 0L;
    public void set(long l) { vl = l; }           // 單次寫
    public long get() { return vl; }              // 單次讀
    public void increment() { vl++; }             // 復合操作:讀+改+寫 (非原子!)
}

// 等價的鎖版本
class LockExample {
    long vl = 0L;
    public synchronized void set(long l) { vl = l; }
    public synchronized long get() { return vl; }
    public void increment() { 
        long temp = get();  // 已同步的讀
        temp += 1L;         // 普通寫
        set(temp);          // 已同步的寫
    }
}

關鍵區別:volatile 僅保證單次讀/寫原子性,不保證復合操作 (如 vl++) 的原子性。

happens-before 關係與內存語義

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;      // 1
        flag = true; // 2 (volatile 寫)
    }

    public void reader() {
        if (flag) {  // 3 (volatile 讀)
            int i = a; // 4
        }
    }
}

happens-before 鏈路:

  • 程序順序規則:1 hb 2,3 hb 4
  • volatile 規則:2 hb 3
  • 傳遞性:1 hb 4

結果:線程 A 寫 volatile 前的所有共享變量修改,對線程 B 讀同一 volatile 後可見。

volatile 寫/讀的內存語義細節

操作內存語義
volatile 寫 將當前線程本地記憶體中共享變量刷新到主記憶體
volatile 讀 將當前線程本地記憶體設為無效,強制從主記憶體讀取

通俗理解:寫是發送消息,讀是接收消息,主記憶體是郵箱。

volatile 不保證原子性:計數器失敗案例

public class Counter {
    public volatile int count = 0;
    
    public void increment() { count++; }  // 非原子!
    
    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) c.increment();
            });
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println(c.count);  // 輸出 < 10000,非 10000
    }
}

原因:count++ 包含讀取、加一、寫入三步,可能被線程交錯執行。

正確的原子性解決方案

方案一:synchronized

public synchronized void increment() { count++; }

方案二:ReentrantLock

private final Lock lock = new ReentrantLock();
public void increment() {
    lock.lock();
    try { count++; } finally { lock.unlock(); }
}

方案三:AtomicInteger (推薦,CAS 無鎖)

private final AtomicInteger count = new AtomicInteger(0);
public void increment() { count.getAndIncrement(); }

volatile 保證有序性:禁止指令重排序


// x、y 為普通變量,flag 為 volatile
x = 2;        // 語句 1
y = 0;        // 語句 2
flag = true;  // 語句 3 (volatile 寫)
x = 4;        // 語句 4
y = -1;       // 語句 5

規則:

  • 不會將語句 3 移到語句 1、2 之後
  • 不會將語句 3 移到語句 4、5 之前
  • 執行到語句 3 時,語句 1、2 必已完成且結果可見
  • 語句 1、2 之間、語句 4、5 之間順序不保證

實戰雙重檢查鎖定

public class Singleton {
    private volatile static Singleton instance = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次檢查 (無鎖)
            synchronized (Singleton.class) {       // 加鎖
                if (instance == null) {            // 第二次檢查
                    instance = new Singleton();    // 初始化
                }
            }
        }
        return instance;
    }
}

為什麼需要 volatile? 防止 instance = new Singleton() 指令重排序導致其他線程看到半初始化對象。

總結:volatile 適用場景

必須同時滿足:

  1. 寫入操作不依賴當前值 (如 true/false、狀態標記)
  2. 該變量未包含在與其他變量的不變式中

典型用途:狀態標記、雙重檢查鎖、簡單的發布-訂閱模式。不可替代:計數器、複雜邏輯、需要原子性的復合操作。

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