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

Java 多執行緒深度解析:volatile 關鍵字與記憶體模型

     編輯:Java 綜合教學

Java 多執行緒深度解析:volatile 關鍵字與記憶體模型

前言

當僅需同步讀寫一兩個實例欄位時,使用 synchronized 顯得過於笨重。volatile 關鍵字為實例欄位提供了無鎖同步機制。宣告欄位為 volatile 後,編譯器與虛擬機即知該欄位可能被其他執行緒併發更新。在深入探討 volatile 之前,我們需要先理解 Java 記憶體模型與併發程式設計的三大特性:原子性、可見性與有序性。

1. Java 記憶體模型與三大特性

Java 記憶體模型(JMM)規定所有變數儲存在主記憶體,每個執行緒擁有自己的工作記憶體。執行緒對變數的所有操作必須在工作記憶體中進行,無法直接操作主記憶體,也無法存取其他執行緒的工作記憶體。

以 int i = 3; 為例,執行緒必須先在工作記憶體的快取行中對變數 i 賦值,再寫入主記憶體,而非直接將數值寫入主記憶體。

原子性

基本資料型別的讀取與賦值操作具備原子性(不可中斷)。但以下操作不具備原子性:

  • y = x; - 包含讀取 x、寫入工作記憶體兩個操作
  • x++; / x = x + 1; - 包含讀取、加 1、寫入三個操作

java.util.concurrent.atomic 套件提供了 AtomicInteger 等類別,利用機器級指令(CAS)保證複合操作的原子性,適合作為共享計數器。

可見性

可見性指一個執行緒修改的狀態對其他執行緒立即可見。volatile 修飾的共享變數,修改值會立即刷新至主記憶體,其他執行緒讀取時會從主記憶體獲取新值。普通共享變數無法保證寫入主記憶體的時機,故不具備可見性。

有序性

JMM 允許編譯器與處理器對指令重排序,不影響單執行緒執行,但可能破壞多執行緒正確性。volatile 可禁止指令重排序,synchronized 與 Lock 透過強制串列執行同步程式碼來保證有序性。

2. volatile 關鍵字的兩層語義

共享變數(類成員變數、靜態成員變數)被 volatile 修飾後,具備:

  1. 保證可見性:一個執行緒修改值,新值對其他執行緒立即可見
  2. 禁止指令重排序

經典案例:停止旗標

// 執行緒 1
boolean stop = false;
while (!stop) {
    doSomething();
}

// 執行緒 2
stop = true;

若無 volatile,執行緒 1 可能永遠讀取到工作記憶體中的舊值 false,導致無限迴圈。加上 volatile 後:修改值強制立即寫入主記憶體;導致其他執行緒該變數的快取行失效;迫使重新從主記憶體讀取。

volatile 無法保證原子性

public class Test {
    public volatile int inc = 0;
    public void increase() { inc++; }
    // 多執行緒並發呼叫 increase() 1000 次,最終結果小於 10000
}

inc++ 包含讀取、加 1、寫入三個子操作,volatile 無法將其合併為原子操作。需使用 AtomicInteger 或 synchronized。

volatile 的有序性保證

volatile 禁止指令重排序的兩層含義:

  • 讀/寫 volatile 變數前的操作已全部完成且結果可見;後續操作尚未開始
  • 優化時不可將 volatile 變數存取語句移至其後,也不可將後續語句移至其前

3. 正確使用 volatile 的條件

volatile 無法取代 synchronized,因為不保證原子性。使用 volatile 必須滿足:

  1. 對變數的寫入操作不依賴當前值(排除 i++、i-- 等)
  2. 該變數未包含在與其他變數的不變式中

反例:不變式破壞

public class NumberRange {
    private volatile int lower, upper;
    public void setLower(int value) {
        if (value > upper) throw new IllegalArgumentException(...);
        lower = value;
    }
    public void setUpper(int value) {
        if (value < lower) throw new IllegalArgumentException(...);
        upper = value;
    }
}

若初始狀態 (0, 5),執行緒 A 呼叫 setLower(4),執行緒 B 呼叫 setUpper(3),兩者交叉執行皆通過檢查,最終狀態變為 (4, 3) 違反不變式。此場景需使用同步。

4. 適用 volatile 的兩大經典場景

狀態旗標

volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() {
    while (!shutdownRequested) { /* do stuff */ }
}

旗標不依賴其他狀態,volatile 簡化編碼且效能優於 synchronized。

雙重檢查鎖定(DCL)單例模式

public class Singleton {
    private volatile static Singleton instance = null;
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile 禁止指令重排序,確保 instance = new Singleton() 的三步驟(分配記憶體、初始化物件、指向記憶體位址)不被重排,避免其他執行緒取得未初始化完成的物件。

建議:《Java 併發編程實踐》推薦使用靜態內部類單例模式替代 DCL,線程安全且延遲初始化:

public class Singleton {
    private Singleton() {}
    public static Singleton getInstance() { return SingletonHolder.sInstance; }
    private static class SingletonHolder {
        private static final Singleton sInstance = new Singleton();
    }
}

5. 總結

volatile 是簡單但脆弱的同步機制,嚴格遵循使用條件(變數獨立於其他變數與先前值)時,可在特定場景替代 synchronized 簡化程式碼。但 volatile 程式碼更易出錯,除非確定適用上述兩大場景,否則優先使用鎖機制。

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