多線程協作的核心難題在於共享數據的正確訪問。Java 提供 volatile 與 synchronized 兩大關鍵字,分別解決可見性與原子性+可見性。本文系統講解線程內存模型、鎖機制、同步粒度選擇、線程安全集合包裝等實戰知識。
所有線程共享堆內存(靜態字段、實例字段、數組元素)。但現代 CPU 為了性能引入緩存、寄存器、指令重排,導致:
示例:計時器線程置 finished=true,計算線程輪詢該標誌。若無 volatile,計算線程可能永遠看不到更新,陷入死循環。
volatile int i; i++ 仍非原子)// 實例方法鎖 this
public synchronized void setXY(int x, int y) { ... }
// 等價塊
public void setXY(int x, int y) { synchronized(this) { ... } }
// 靜態方法鎖 Class 對象
public static synchronized void staticMethod() { ... }
// 顯式指定鎖
private final Object lock = new Object();
public void method() { synchronized(lock) { ... } }
關鍵認知:鎖保護的是「代碼塊進入權」,而非變量本身。不同實例調用同步方法互不阻塞;同一實例不同同步方法互斥。
| 原則 | 說明 |
|---|---|
| 縮小同步範圍 | 僅保護必要的共享狀態操作,避免 I/O、睡眠在鎖內 |
| 不保護局部變量 | 棧上私有,天然線程安全 |
| 優先用 JDK 線程安全類 | ConcurrentHashMap、CopyOnWriteArrayList、AtomicInteger 等 |
| 集合包裝器 | Collections.synchronizedMap/List/Set 為非安全集合加鎖 |
public class SimpleCache {
private final Map cache = new HashMap<>();
public Object getObject(String key) {
synchronized (cache) { // 以 cache 作為鎖
Object val = cache.get(key);
if (val == null) {
val = load(key); // 昂貴操作在鎖內,簡單但降低併發度
cache.put(key, val);
}
return val;
}
}
public void clearCache() { synchronized (cache) { cache.clear(); } }
}
改進方向:雙重檢查鎖、ConcurrentHashMap.computeIfAbsent、FutureTask 避免重複加載。
Vector/Hashtable 方法雖同步,但複合操作(如「檢查後取」)仍需外部加鎖理解 JMM 可見性與原子性區別,volatile 管單變量發布,synchronized 管臨界區原子性。實戰中優先用 java.util.concurrent 成熟組件,僅在必要時手寫同步,並嚴守「鎖保護狀態、狀態決定鎖」設計原則。