在 Java 並發編程中,volatile 是一種輕量級的同步機制。當變量被聲明為 volatile 類型後,編譯器與運行時都會避免將該變量的操作與其他內存操作重排序。在特定場景下,合理使用 volatile 可替代鎖機制,減少代碼複雜度並提升可讀性。
當一條線程修改 volatile 變量時,其他線程能立即看到最新值。即讀取 volatile 變量總是返回最近一次寫入的值。
對單個 volatile 變量的讀寫具有原子性(包括 long 和 double 類型)。但對於 i++ 這類復合操作(讀取-修改-寫入),volatile 不保證 原子性。
volatile 的可見性基於 JMM(Java 內存模型)的 happens-before 規則實現。JMM 定義了八條天然的 happens-before 規則,若兩個操作無法從這些規則推導出順序關係,虛擬機可對其隨意重排序。
其中「volatile 變量規則」規定:對一個 volatile 變量的寫操作,先行發生於後續對該變量的讀操作。此規則保證虛擬機不會對 volatile 讀/寫操作進行重排序。
public class VolatileTest extends Thread {
private boolean isRunning = true;
public boolean isRunning() { return isRunning; }
public void setRunning(boolean isRunning) { this.isRunning = isRunning; }
public void run() {
System.out.println("進入了 run...............");
while (isRunning) {}
System.out.println("isRunning 被修改為 false,線程將停止");
}
public static void main(String[] args) throws InterruptedException {
VolatileTest volatileThread = new VolatileTest();
volatileThread.start();
Thread.sleep(1000);
volatileThread.setRunning(false); // 停止線程
}
}
輸出結果:
進入了 run...............
程序未輸出停止信息,說明主線程修改 isRunning 對工作線程不可見。
JMM 規定所有變量存儲在主內存,每條線程有自己的工作內存(緩存副本)。線程操作變量時在工作內存進行,完成後刷新回主內存。例中主線程修改 isRunning 並刷新回主內存,但工作線程仍讀取工作內存中的舊值,導致可見性問題。
給 isRunning 加上 volatile 關鍵字。聲明為 volatile 後:
volatile 不保證復合操作的原子性。示例:
public class VolatileTest1 {
private volatile int value;
public void increment() {
value++;
System.out.println(value);
}
public static void main(String[] args) {
final VolatileTest1 test = new VolatileTest1();
for (int i = 0; i < 10; i++) {
new Thread(() -> test.increment()).start();
}
}
}
輸出(示例): 1 6 5 4 2 3 8 7 9 10 — 結果非預期的 1~10 遞增,出現線程安全問題。
value++ 非原子操作,實際分三步:
value 到工作內存value + 1多線程並發時,線程 1 讀取後未寫回,線程 2 也讀取舊值,導致更新丟失。
increment() 加鎖(synchronized 或 Lock)AtomicInteger)| 維度 | volatile | synchronized |
|---|---|---|
| 修飾範圍 | 只能修飾變量 | 可修飾變量、方法、代碼塊 |
| 阻塞性 | 不會導致線程阻塞 | 可能導致線程阻塞 |
| 可見性 | 保證 | 保證 |
| 原子性 | 不保證復合操作 | 保證 |
| 核心解決問題 | 多線程間變量可見性 | 多線程間資源訪問同步性 |