若所有存取都是「讀取」,通常不需同步;只要有「寫入」,就必須保證同一時間只有一個執行緒操作。
public class DirtyReadDemo extends Thread {
public static int n = 0;
public void run() {
int m = n; // 讀取
Thread.yield(); // 主動讓出 CPU,放大競態機率
m++;
n = m; // 寫回
}
public static void main(String[] args) throws Exception {
DirtyReadDemo demo = new DirtyReadDemo();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) threads[i] = new Thread(demo);
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
System.out.println("n = " + n); // 預期 100,實際常 < 100
}
}
輸出範例:n = 59。
Java 編譯器將 n++ 編譯為多條 bytecode:
aload_0 // 載入 this
dup // 複製引用
getfield // 讀取 n
iconst_1 // 常數 1
iadd // 整數加法
putfield // 寫回 n
對應「複製到工作記憶體 → 運算 → 寫回主記憶體」三步驟,中間任何一步被切換都會產生競態。
執行緒操作共享變數流程:主記憶體 → 工作記憶體 → 運算 → 寫回主記憶體。無同步時,副本何時刷新不可預期,導致可見性與原子性問題。
| 方案 | 關鍵字/類別 | 適用場景 |
|---|---|---|
| 同步方法 | synchronized 修飾方法 | 整個方法需互斥 |
| 同步區塊 | synchronized(obj) { ... } | 縮小鎖範圍,提高並發 |
| volatile | volatile 修飾變數 | 僅保證可見性,不保證複合操作原子性 |
| 原子類別 | AtomicInteger 等 | 無鎖 CAS,高併發計數器首選 |
| Lock | ReentrantLock | 需可中斷、公平鎖、條件變數等進階功能 |