volatile 是 Java 並發編程中「輕量級同步機制」,常被誤解或濫用。本文從 JMM 內存模型語義、正確使用條件、五大經典模式、高級讀寫鎖策略、性能對比、常見誤區六個維度,徹底講透 volatile 的正確用法。
| 語義 | 說明 | 實現機制 |
|---|---|---|
| 可見性 | 一個線程對 volatile 變量的寫入,對其他線程立即可見 | 寫入時插入 StoreStore + StoreLoad 內存屏障;讀取時插入 LoadLoad + LoadStore 屏障 |
| 有序性 | 禁止指令重排序:volatile 寫前的操作不會重排到寫後;volatile 讀後的操作不會重排到讀前 | 編譯器屏障 + CPU 內存屏障 (x86: lock addl $0,0(%rsp) / ARM: dmb) |
// volatile 不保證複合操作原子性
volatile int count = 0;
// count++ 實際是三步非原子操作:
// 1. 讀取 count (volatile read)
// 2. 增加 1 (ALU 運算)
// 3. 寫入 count (volatile write)
// 多線程並發 count++ 會丟失更新
// 解決:AtomicInteger / synchronized / LongAdder
// volatile 寫入 happens-before 後續對同一變量的讀取
// 線程 A 線程 B
volatile boolean flag = false;
int a = 0; int b = 0;
a = 1; // 1. 普通寫
flag = true; // 2. volatile 寫 (StoreStore/StoreLoad 屏障)
// └── happens-before ──►
// 3. volatile 讀 (LoadLoad/LoadStore 屏障)
if (flag) {
b = a; // 4. 保證讀到 a=1 (可見性 + 有序性)
}
// 關鍵:flag=true 之前的所有普通寫入 (a=1) 對 flag 讀取後的線程可見
只有同時滿足以下兩個條件,volatile 才能安全替代 synchronized:
// 優雅關閉、初始化完成、錯誤標記
public class ServiceController {
private volatile boolean shutdownRequested = false;
private volatile boolean initialized = false;
private volatile Throwable initializationError;
public void start() {
new Thread(() -> {
try {
doInitialize();
initialized = true; // volatile 寫:發布初始化完成
} catch (Throwable t) {
initializationError = t; // 發布錯誤
}
}).start();
}
public void shutdown() {
shutdownRequested = true; // volatile 寫:請求關閉
}
public void doWork() {
while (!shutdownRequested) { // volatile 讀:檢查關閉信號
// 執行任務
}
}
public boolean isInitialized() {
return initialized; // volatile 讀
}
public Throwable getInitializationError() {
return initializationError;
}
}
// 發布不可變對象 / 安全初始化對象
public class ConfigManager {
// volatile 保證:引用可見 + 對象內部狀態可見 (無重排)
private volatile AppConfig config;
public void loadConfigAsync() {
new Thread(() -> {
AppConfig newConfig = parseConfigFile(); // 耗時解析
// 關鍵:構造完成後單次寫入
config = newConfig; // volatile 寫:安全發布
}).start();
}
public AppConfig getConfig() {
return config; // volatile 讀:獲取最新配置 (或 null)
}
}
// 條件:AppConfig 必須是不可變對象,或發布後不再修改
public final class AppConfig {
private final String dbUrl;
private final int poolSize;
private final Map properties; // 必須不可變集合
// 構造器初始化所有 final 字段
public AppConfig(String dbUrl, int poolSize, Map props) {
this.dbUrl = dbUrl;
this.poolSize = poolSize;
this.properties = Collections.unmodifiableMap(new HashMap<>(props));
}
}
// 定期發布只讀快照:傳感器數據、統計指標、最後用戶
public class MetricsCollector {
// volatile 發布不可變快照對象
private volatile MetricsSnapshot latestSnapshot;
@Scheduled(fixedRate = 1000)
public void collect() {
MetricsSnapshot snapshot = new MetricsSnapshot(
System.currentTimeMillis(),
meterRegistry.get("http.requests").counter().count(),
meterRegistry.get("jvm.memory.used").gauge().value(),
threadPool.getActiveCount()
);
latestSnapshot = snapshot; // volatile 寫:發布新快照
}
public MetricsSnapshot getLatest() {
return latestSnapshot; // volatile 讀:獲取最新快照
}
}
// 快照對象必須不可變
public final class MetricsSnapshot {
public final long timestamp;
public final long requestCount;
public final double memoryUsed;
public final int activeThreads;
public MetricsSnapshot(long timestamp, long requestCount, double memoryUsed, int activeThreads) {
this.timestamp = timestamp;
this.requestCount = requestCount;
this.memoryUsed = memoryUsed;
this.activeThreads = activeThreads;
}
}
// 所有字段 volatile + 簡單 Getter/Setter + 無不變式
// 適用於:HttpSession 屬性、配置持有者、簡單數據傳輸
public class UserSession {
private volatile String userId;
private volatile String username;
private volatile int loginCount;
private volatile long lastAccessTime;
private volatile Map attributes; // 必須不可變 Map
// 簡單 Getter/Setter,無邏輯
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public int getLoginCount() { return loginCount; }
public void setLoginCount(int loginCount) { this.loginCount = loginCount; }
public long getLastAccessTime() { return lastAccessTime; }
public void setLastAccessTime(long lastAccessTime) { this.lastAccessTime = lastAccessTime; }
public Map getAttributes() { return attributes; }
public void setAttributes(Map attributes) {
this.attributes = Collections.unmodifiableMap(new HashMap<>(attributes));
}
}
// 注意:集合/數組字段 volatile 只保證引用可見,不保證內部元素可見
// 必須整體替換不可變集合
// 讀多寫少場景:volatile 保證讀路徑可見性,synchronized 保證寫路徑原子性
public class CheesyCounter {
// 核心:volatile 修飾值,保證讀取可見
@GuardedBy("this")
private volatile int value;
// 讀路徑:無鎖,直接 volatile 讀 (極快)
public int getValue() {
return value; // volatile read:無鎖、無阻塞、高並發
}
// 寫路徑:同步保證原子性
public synchronized int increment() {
return ++value; // volatile write:在同步塊內,可見性由鎖隱含保證
}
public synchronized int decrement() {
return --value;
}
public synchronized int add(int delta) {
value += delta;
return value;
}
}
// 性能對比 (讀多寫少 100:1):
// - 全 synchronized:讀需獲取鎖,爭用嚴重
// - ReentrantReadWriteLock:讀鎖仍有 CAS 開銷
// - CheesyCounter:讀路徑僅 volatile 讀,接近無同步性能
//
// 適用條件:寫操作極少、讀操作極多、計數器/開關/配置值
// 風險:擴展性差,新增寫操作易破壞同步協議
public class Singleton {
private static Singleton instance; // 無 volatile!
public static Singleton getInstance() {
if (instance == null) { // 檢查 1
synchronized (Singleton.class) {
if (instance == null) { // 檢查 2
instance = new Singleton(); // 非原子:分配內存 → 初始化 → 賦值引用
}
}
}
return instance;
}
}
// 問題:instance = new Singleton() 可能重排為:
// 1. memory = allocate() // 分配內存
// 2. instance = memory // 賦值引用 (此時對象未初始化!)
// 3. initialize(memory) // 初始化對象
// 線程 B 看到 instance != null 但對象未初始化 → NPE/錯誤狀態
public class Singleton {
// volatile 禁止重排:確保初始化完成前不發布引用
private static volatile Singleton instance;
public static Singleton getInstance() {
Singleton result = instance; // 讀取到局部變量 (減少 volatile 讀次數)
if (result == null) { // 第一次檢查 (無鎖)
synchronized (Singleton.class) {
result = instance; // 第二次檢查 (鎖內)
if (result == null) {
instance = result = new Singleton(); // volatile 寫:發布完全初始化的對象
}
}
}
return result;
}
}
// 原理:volatile 寫入插入 StoreStore 屏障,確保:
// 1. 對象內存分配
// 2. 對象字段初始化 (final 字段初始化有特殊語義)
// 3. 引用賦值給 instance
// 順序不可重排,其他線程 volatile 讀取時必見完全初始化對象
| 操作 | 指令序列 | 大致週期 | 特點 |
|---|---|---|---|
| 普通讀/寫 | MOV | 1-3 cycles | 無屏障,可能見舊值 |
| volatile 讀 | MOV + LoadLoad + LoadStore | 3-10 cycles | 無鎖,無緩存行鎖定 |
| volatile 寫 | StoreStore + StoreLoad + MOV | 10-40 cycles | StoreLoad 最貴 (mfence/lock addl),刷新存儲緩衝區 |
| synchronized 進入/退出 | monitorenter / monitorexit (CAS + OS 互斥鎖) | 20-100+ cycles | 無競爭輕量級鎖 (偏向鎖/輕量級鎖);競爭膨脹重量級鎖 |
| AtomicInteger.getAndIncrement() | CAS 循環 (lock cmpxchg) | 10-50 cycles | 無鎖 CAS,高爭用時自旋開銷大 |
// 讀多寫少 (100:1) 並發場景:
// - synchronized:所有線程爭搶同一把鎖,吞吐量隨線程數下降
// - ReentrantReadWriteLock:讀鎖共享,但獲取/釋放有 CAS 開銷
// - volatile 讀:無鎖、無 CAS、無緩存行爭用,線性伸縮
// 寫多讀少場景:
// - volatile 寫:StoreLoad 屏障開銷固定
// - synchronized:競爭嚴重時上下文切換開銷巨大
// - Atomic:CAS 失敗重試,高爭用效率低
// 結論:volatile 適合「讀多寫少、單變量、無不變式」場景
Benchmark Mode Cnt Score Error Units
VolatileReadBenchmark.read thrpt 10 45000 ± 1200 ops/ms // 單線程讀
VolatileWriteBenchmark.write thrpt 10 12000 ± 800 ops/ms // 單線程寫
SynchronizedBenchmark.read thrpt 10 38000 ± 1500 ops/ms // 無競爭鎖讀
SynchronizedBenchmark.write thrpt 10 9000 ± 600 ops/ms // 無競爭鎖寫
AtomicBenchmark.increment thrpt 10 8000 ± 500 ops/ms // CAS 增量
LongAdderBenchmark.increment thrpt 10 25000 ± 1000 ops/ms // 長加器 (高爭用最優)
// 8 線程並發讀 (volatile vs synchronized):
VolatileReadBenchmark.read thrpt 10 320000 ± 5000 ops/ms // 近線性伸縮
SynchronizedBenchmark.read thrpt 10 45000 ± 2000 ops/ms // 鎖爭用瓶頸
| 誤區 | 錯誤代碼 | 問題分析 | 正確方案 |
|---|---|---|---|
| 用 volatile 做計數器 | volatile int count; count++; | 非原子複合操作,丟失更新 | AtomicInteger / LongAdder / synchronized |
| 用 volatile 保護多變量不變式 | volatile int lower, upper; // lower <= upper | 兩變量無原子性保證,競態條件 | synchronized / 封裝為不可變對象一次性發布 |
| volatile 數組/集合元素可見 | volatile int[] arr; arr[0] = 1; | volatile 只保證引用可見,不保證元素寫入可見 | AtomicIntegerArray / CopyOnWriteArrayList / 整體替換不可變集合 |
| volatile 替代 synchronized 所有場景 | volatile boolean flag; synchronized(method) {...} | 無法實現互斥、複合操作原子性 | 明確區分:volatile 用於單變量發布/標記,synchronized 用於臨界區/不變式 |
| DCL 未加 volatile | static Singleton instance; | 指令重排導致部分初始化對象發布 | static volatile Singleton instance; |
| 以為 volatile 讀寫是原子的 | volatile long x; x = 1L; // 32位 JVM 非原子 | long/double 非 volatile 時 32位 JVM 可能分兩次 32位寫入 | volatile 保證 64位 讀寫原子 (JMM 規範),但複合操作仍非原子 |
volatile是 Java 並發工具箱中的「手術刀」——極其鋒利但極其危險。它通過內存屏障提供可見性與有序性,不提供原子性。只有在單變量、寫入不依賴舊值、無多變量不變式的嚴格條件下,才能安全替代 synchronized 獲得性能優勢。絕大多數業務場景,優先選擇 JUC 成熟組件,將 volatile 保留給框架開發、基礎設施代碼、極致性能優化場景。