在多線程程式設計中,若對共享的可變數據訪問缺乏同步機制,後果往往十分嚴重,即使該變量具備原子讀寫特性也不例外。本文將透過具體代碼示例,深入剖析線程同步的核心問題,並展示如何使用 synchronized 與 volatile 關鍵字正確解決數據可見性問題。
考慮以下程式片段,主線程啟動一個後台線程執行計數任務,約 1 秒後嘗試通過設置 stopRequested 標誌來停止該線程:
import java.util.concurrent.TimeUnit;
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
System.out.println(i);
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
直覺上程式應在 1 秒後結束,但實際執行時後台線程會無限循環。原因在於:後台線程無法看到主線程對 stopRequested 的修改,這就是未同步導致的「數據不可見」問題。
為確保讀寫操作的原子性與可見性,必須對讀寫方法均加鎖:
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested()) {
i++;
System.out.println(i);
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
關鍵點: 寫方法(requestStop)與讀方法(stopRequested)皆需聲明 synchronized,否則無法構成真正的同步語義。
volatile 關鍵字能保證變量對所有線程的可見性,且禁止指令重排,是解決此類問題的輕量級方案:
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
System.out.println(i);
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
此寫法碼簡潔、語義清晰,適用於單一變量的狀態標誌場景。
synchronized 需對讀寫雙方同步,開銷較大但功能完整。volatile 適合單變量狀態標誌,無鎖開銷,效能更優。