當僅需同步讀寫一兩個實例欄位時,使用 synchronized 顯得過於笨重。volatile 關鍵字為實例欄位提供了無鎖同步機制。宣告欄位為 volatile 後,編譯器與虛擬機即知該欄位可能被其他執行緒併發更新。在深入探討 volatile 之前,我們需要先理解 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 透過強制串列執行同步程式碼來保證有序性。
共享變數(類成員變數、靜態成員變數)被 volatile 修飾後,具備:
// 執行緒 1
boolean stop = false;
while (!stop) {
doSomething();
}
// 執行緒 2
stop = true;
若無 volatile,執行緒 1 可能永遠讀取到工作記憶體中的舊值 false,導致無限迴圈。加上 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 無法取代 synchronized,因為不保證原子性。使用 volatile 必須滿足:
i++、i-- 等)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) 違反不變式。此場景需使用同步。
volatile boolean shutdownRequested;
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) { /* do stuff */ }
}
旗標不依賴其他狀態,volatile 簡化編碼且效能優於 synchronized。
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();
}
}
volatile 是簡單但脆弱的同步機制,嚴格遵循使用條件(變數獨立於其他變數與先前值)時,可在特定場景替代 synchronized 簡化程式碼。但 volatile 程式碼更易出錯,除非確定適用上述兩大場景,否則優先使用鎖機制。