前言
並發編程的本質是**在共享數據、多執行緒、有限資源**三約束下協調執行。Java 記憶體模型 (JMM) 定義了**線程與主內存交互規範**,是理解 `volatile`、`synchronized`、`final`、並發容器語義的理論基石。本文系統剖析**五大核心特性、JMM happens-before 規則、指令重排類型、同步工具語義、實戰避坑**,構建並發編程知識骨架。
一、五大核心特性深度解析
1. 共享性
- 定義:多執行緒可訪問同一堆內存變量 (實例域、靜態域、數組元素)。
- 本質:堆內存全局可見,棧幀線程私有。
- 風險:非線程安全操作 (如 `count++`) 導致**競態條件**。
- 對策**:封閉共享** (ThreadLocal、棧封閉、不可變對象) → **同步訪問** (鎖、CAS、並發容器)。
2. 互斥性
- 定義:同一時刻僅允許一個執行緒進入臨界區。
- 實現**:監視器鎖** (`synchronized`)、**顯式鎖** (`ReentrantLock`)、**信號量** (`Semaphore`)、**讀寫鎖** (`ReentrantReadWriteLock`)。
- 粒度**:對象鎖、類鎖、方法鎖、代碼塊鎖、分段鎖 (ConcurrentHashMap JDK 7)、分離鎖 (Striped)。
- 活躍性風險**:死鎖、活鎖、飢餓 → **按序獲取鎖、超時機制、避免嵌套鎖**。
3. 原子性
- 定義**:操作不可分割,要麼全執行、要麼全不執行,無中間狀態可見。
- 基本型原子性**:`long`/`double` 非 `volatile` 時 **64 位寫入非原子** (JMM 允許拆兩次 32 位寫入);其他基本型、引用賦值原子。
- 複合操作非原子**:`i++` (讀-改-寫)、`check-then-act` (如 `if (map.get(key)==null) map.put(key,val)`)。
- 實現方案**:
- **鎖** (`synchronized`/`Lock`):悲觀、阻塞、簡單通用。
- **CAS** (`Atomic*`、`LongAdder`、`Unsafe.compareAndSwap*`):樂觀、非阻塞、高併發優、ABA 問題 (用 `AtomicStampedReference` 解決)。
- **不可變對象** (`final`、`String`、`Record`):天然原子、無鎖。
4. 可見性
- 定義**:一個執行緒對共享變量的修改,對其他執行緒**立即可見**。
- JMM 工作內存模型**:每個執行緒有**工作內存** (CPU 緩存/寄存器副本),讀寫先經工作內存,再同步回主內存。
- 導致不可見**:編譯器優化 (寄存器緩存)、CPU 緩存一致性協議延遲、指令重排。
- 保證可見性**:
- `volatile`:寫入**立即刷回主存** + **禁止指令重排** (StoreStore/StoreLoad 屏障)。
- `synchronized`:解鎖前**刷回主存**,加鎖後**失效工作內存重讀**。
- `final`:構造函數結束前**寫入可見**,無需同步 (JMM 特殊規則)。
- 並發容器 (`ConcurrentHashMap`、`CopyOnWriteArrayList`):內部 `volatile`/`Unsafe` 保證。
5. 有序性
- 定義**:程序執行順序符合代碼邏輯順序 (單執行緒語義)。
- 重排序類型**:
- **編譯器優化重排** (JIT):無數據依賴調換順序。
- **指令級並行重排** (CPU):流水線、超標量、亂序執行。
- **內存系統重排** (緩存/寫緩衝區):寫入不同緩存行順序不定。
- as-if-serial 語義**:單執行緒下重排不改變結果 (含異常)。
- 保證有序性**:
- `volatile`:寫屏障 (StoreStore/StoreLoad) + 讀屏障 (LoadLoad/LoadStore)。
- `synchronized`:鎖語義隱含 `happens-before` (解鎖 hb 後續加鎖)。
- `final`:構造函數內寫入 hb 構造函數返回 (安全發布)。
二、JMM Happens-Before 八大規則
| 規則 | 描述 | 典型應用 |
| 程序順序 | 單執行緒代碼順序 hb | 基礎 |
| 監視器鎖 | 解鎖 hb 後續同鎖加鎖 | `synchronized` 可見性/有序性 |
| volatile 變量 | 寫 hb 後續讀 | 狀態標誌、DCL |
| 線程啟動 | `Thread.start()` hb 線程內動作 | 線程安全發布 |
| 線程終止 | 線程內動作 hb `Thread.join()` 返回 | 主線程獲取結果 |
| 線程中斷 | `interrupt()` hb 被中斷線程檢測到 | 優雅關閉 |
| final 域 | 構造函數寫 final hb 構造函數返回 hb 讀 final | 不可變對象安全發布 |
| 傳遞性 | A hb B, B hb C → A hb C | 鏈式推導 |
三、經典並發 Bug 復現與修復
1. 計數器非原子
// Bug
int count = 0;
for (int i=0;i<10;i++) new Thread(()->{ for(int j=0;j<1000;j++) count++; }).start();
// 結果 < 10000
// Fix 1: AtomicInteger
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
// Fix 2: LongAdder (高併發更優)
LongAdder count = new LongAdder();
count.increment();
2. 雙重檢查鎖 (DCL) 半初始化
// Bug (無 volatile)
static Singleton instance;
public static Singleton get() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton(); // 分配→初始化→賦值,可能重排
}
}
return instance;
}
// Fix: volatile 禁止重排
static volatile Singleton instance;
3. 發布逃逸 (this 逃逸)
// Bug: 構造函數中啟動線程/註冊監聽器,this 未完全構造即可見
public class EventSource {
public EventSource() {
new Thread(() -> doSomething(this)).start(); // 危險!
}
}
// Fix: 工廠方法 + 完全構造後發布
public static EventSource create() {
EventSource es = new EventSource();
new Thread(() -> doSomething(es)).start();
return es;
}
四、同步工具語義速查表
| 工具 | 互斥 | 可見性 | 有序性 | 適用場景 |
| `synchronized` | ✅ | ✅ | ✅ | 通用臨界區、方法級同步 |
| `volatile` | ❌ | ✅ | ✅ | 狀態標誌、單次寫入、DCL |
| `ReentrantLock` | ✅ | ✅ | ✅ | 可中斷、超時、公平、條件變量 |
| `Atomic*` | ✅ (CAS) | ✅ | ✅ | 計數器、引用更新、累加器 |
| `LongAdder` | ✅ (CAS+分段) | ✅ | ✅ | 高併發計數、統計指標 |
| `ConcurrentHashMap` | 分段/節點 CAS | ✅ | ✅ | 高併發 Map、緩存 |
| `CopyOnWriteArrayList` | 寫時複製 | ✅ | ✅ | 讀多寫少、快照遍歷 |
| `ThreadLocal` | 隔離 (無共享) | - | - | 線程封閉、上下文傳遞 |
五、實戰最佳實踐清單
- 優先用不可變對象:`final` 域、Record、防禦性複製、集合 `Collections.unmodifiable*`。
- 縮小鎖範圍:只鎖核心邏輯,避免 I/O、日志、RPC 在鎖內。
- 用並發容器替代手工同步:`ConcurrentHashMap`、`LongAdder`、`BlockingQueue`。
- 避免過早優化:先正確、再測壓、再優化熱點 (JFR/Async Profiler 定位)。
- 線程池必配有界隊列 + 拒絕策略:防 OOM、防任務積壓、支撐背壓。
- 虛擬線程 (JDK 21+) 改寫阻塞 IO**:`Executors.newVirtualThreadPerTaskExecutor()`,無需 Reactive 改造。
- 嚴禁雙重檢查鎖無 volatile**:JDK 5+ 必須加 `volatile`。
- 發布對象必須安全**:構造函數不啟動線程、不註冊回調、用工廠/構建器模式。
💡 核心心法:共享是根源,互斥是手段,原子/可見/有序是 JMM 給出的三大保證。寫並發代碼時,先問:**這個變量共享嗎?操作原子嗎?寫入需可見嗎?順序敏感嗎?** 選最簡單滿足條件的工具。**能用不可變別用鎖,能用並發容器別手寫同步,能用虛擬線程別手寫異步。**