在 Java 多線程開發中,volatile 與 synchronized 是兩大核心同步機制。雖然兩者都能解決線程安全問題,但其適用場景、語義保證與性能開銷截然不同。本文將通過實戰代碼演示,深入剖析兩者的區別與正確使用姿勢。
首先看一個經典的計數器例子:啟動 10,000 條線程,每條線程對共享變數 count 執行 count++ 操作。
public class VolatileTest extends Thread {
private static int count = 0;
@Override
public void run() {
count++; // 非原子操作:讀取、修改、寫入三步
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new VolatileTest();
threads[i].start();
}
Thread.sleep(1000); // 等待所有線程結束
System.out.println("最終計數:" + count); // 輸出遠小於 10000
}
}
現象:預期輸出 10000,實際輸出往往只有 8000 左右。原因在於 count++ 並非原子操作,包含「讀取、加一、寫回」三步,多線程並發時會相互覆蓋。
很多開發者誤以為加上 volatile 就能解決線程安全問題,我們來驗證一下:
private volatile static int count = 0; // 僅添加 volatile 關鍵字
結果:輸出依然不是 10000。
深度解析:volatile 僅保證可見性(一線程寫入,其他線程立即可見)和有序性(禁止指令重排),但不保證原子性。對於 count++ 這類「先讀後寫」的複合操作,volatile 無法防止線程在讀取到舊值後、寫入新值前被其他線程搶佔修改。
使用 synchronized 同步塊(或方法)可將複合操作封裝為原子單元:
public class SynchronizedTest extends Thread {
private static int count = 0;
private static final Object LOCK = new Object(); // 專用鎖對象
@Override
public void run() {
synchronized (LOCK) { // 同步塊,僅鎖住核心代碼
count++;
}
}
// main 方法同上,輸出穩定為 10000
}
關鍵點:
private static final Object,避免使用 String 或基本類型包裝類(因常量池可能導致非預期共享鎖)。synchronized 同時提供互斥性(同一時刻僅一線程持有鎖)、可見性(解鎖前將工作內存刷新到主內存)、有序性。| 維度 | volatile | synchronized |
|---|---|---|
| 原子性 | ❌ 僅保證單次讀/寫原子 | ✅ 保證代碼塊整體原子 |
| 可見性 | ✅ 立即刷新主內存 | ✅ 解鎖時刷新主內存 |
| 有序性 | ✅ 禁止指令重排 | ✅ 鎖語義天然有序 |
| 阻塞性 | ❌ 非阻塞,輕量級 | ✅ 可能導致線程阻塞/上下文切換 |
| 適用場景 | 狀態標誌、單次賦值、雙重檢查鎖 | 複合操作、臨界區、多變數協同 |
volatile 適用於單一變數的狀態標識場景,例如:
volatile boolean running = true;volatile 保證原子)volatile 是「輕量級的 synchronized」,適用於單一變數、無上下文依賴的簡單場景;synchronized 則是重型武器,解決複合操作、多變數協同的線程安全問題。實戰中:能用 volatile 解決的堅決不用 synchronized,但涉及「先檢查後執行」等複合邏輯時,必須上 synchronized 或 Lock/原子類。