同步容器 vs 並發容器:核心差異
| 維度 | 同步容器 | 並發容器 |
| 代表類別 | Vector、Hashtable、Collections.synchronizedXxx | ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue |
| 實現方式 | 方法級 synchronized,鎖住整個物件 | 分段鎖、CAS、複製於寫、無鎖算法 |
| 併發效能 | 串行執行,擴展性差 | 高併發支援,擴展性強 |
| 複合操作 | 需額外客戶端鎖(如 getLast) | 內建原子複合操作(putIfAbsent 等) |
| 迭代器 | 快速失敗,需外部同步 | 弱一致性,無需鎖定 |
同步容器隱患:複合操作非原子
// 看似安全,實則有競態條件
public static Object getLast(Vector<Object> list) {
int lastIndex = list.size() - 1; // 操作 1
return list.get(lastIndex); // 操作 2
}
public static void deleteLast(Vector<Object> list) {
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
// 多執行緒同時調用:執行緒 A 獲取索引,執行緒 B 刪除元素,A 再 get → IndexOutOfBoundsException
// 解決:客戶端鎖定 list 物件
synchronized(list) { ... }
ConcurrentHashMap 核心架構(JDK 1.8+)
┌─────────────────────────────────────────────────────────┐
│ ConcurrentHashMap │
├─────────────────────────────────────────────────────────┤
│ Node<K,V>[] table (初始容量 16,2的冪次) │
│ - 每個桶頭節點獨立鎖定(synchronized/CAS) │
│ - 擴容支援多執行緒協作遷移 │
└─────────────────────────────────────────────────────────┘
JDK 1.7 vs 1.8 關鍵演進
| 特性 | JDK 1.7 (分段鎖) | JDK 1.8 (CAS + synchronized) |
| 結構 | Segment[] + HashEntry[] | Node[] + TreeBin |
| 鎖粒度 | 16 個 Segment(預設) | 桶級別(每個桶頭節點) |
| Put 操作 | 鎖 Segment | CAS 失敗則 synchronized 鎖桶頭 |
| Get 操作 | 無鎖(volatile) | 無鎖(volatile + 不變性) |
| 擴容 | 單執行緒鎖定 Segment | 多執行緒協作遷移 |
| 樹化 | 不支援 | 鏈表 ≥ 8 且容量 ≥ 64 轉紅黑樹 |
核心操作深度解析
Get 操作:完全無鎖
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) { // 直接命中桶頭
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
} else if (eh < 0) { // 樹節點或轉移節點
return (e instanceof TreeBin) ? ((TreeBin<K,V>)e).get(h, key) : e.get(h, key);
}
// 鏈表遍歷
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
- 利用
volatile 保證可見性
- Node 結構不可變(val/next 為 volatile),遍歷過程安全
- 無需加鎖,極高讀效能
Put 操作:CAS + synchronized 桶鎖
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int h = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 延遲初始化
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
// 1. 桶為空,CAS 嘗試插入
if (casTabAt(tab, i, null, new Node<>(h, key, value, null)))
break; // 成功
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 協助擴容
else {
// 2. 桶非空,鎖定桶頭節點
synchronized (f) {
if (tabAt(tab, i) == f) { // 雙重檢查
if (fh >= 0) { // 鏈表
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
if (!onlyIfAbsent) e.val = value;
return e.val;
}
if (e.next == null) {
e.next = new Node<>(h, key, value, null);
break;
}
e = e.next;
}
} else if (f instanceof TreeBin) { // 紅黑樹
Node<K,V> p = ((TreeBin<K,V>)f).putTreeVal(h, key, value);
if (p != null) return p.val;
}
}
}
// 3. 達閾值樹化
if (binCount != 0 && binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
}
}
addCount(1, binCount); // 更新 size,可能觸發擴容
return null;
}
Size 計算:LongAdder 思想
baseCount + CounterCell[] 分散計數
- 低併發更新
baseCount,高併發分散到 CounterCell
size() 返回近似值,非精確實時值
Remove 操作與弱一致性
刪除不銷毀節點,構建新鏈表
// 刪除節點 C:A → B → C → D
// 新鏈表:A' → B' → D (C 及之前節點複製,C 之後共享)
- 讀執行緒遍歷舊鏈表不受影響(不可變性)
- 寫執行緒構建新鏈表,CAS 替換桶頭
- 實現弱一致性:迭代器反映操作啟動時的狀態,可容忍並發修改
擴容機制:多執行緒協作遷移
- 觸發條件:
size >= threshold 或 put 時發現桶為 ForwardingNode
- 建立新表
nextTable(容量翻倍)
- 每個執行緒領取遷移任務(
transferIndex 原子遞減)
- 遷移完成
CAS 替換 table 引用
- Get/Put 遇到
ForwardingNode 自動轉向新表或協助遷移
關鍵複合操作(原子性保證)
| 方法 | 語義 | 替代同步容器模式 |
putIfAbsent(k, v) | Key 不存在才放入 | if (!map.containsKey(k)) map.put(k, v) |
remove(k, v) | Key 且 Value 匹配才移除 | if (map.get(k).equals(v)) map.remove(k) |
replace(k, oldV, newV) | Key 且舊值匹配才替換 | if (map.get(k).equals(oldV)) map.put(k, newV) |
computeIfAbsent(k, fn) | Key 不存在則計算並放入 | if (!map.containsKey(k)) map.put(k, fn.apply(k)) |
merge(k, v, fn) | Key 存在合併,不存在放入 | 複雜條件更新 |
最佳實踐與選型指南
| 場景 | 推薦容器 | 理由 |
| 高併發 Map,讀多寫少 | ConcurrentHashMap | 無鎖讀、細粒度寫鎖、內建複合操作 |
| 高併發 List,讀多寫少 | CopyOnWriteArrayList | 寫時複製,讀無鎖,適合事件監聽器列表 |
| 高併發 Queue | ConcurrentLinkedQueue | 無鎖 CAS,非阻塞,高吞吐 |
| 阻塞隊列、生產消費 | ArrayBlockingQueue / LinkedBlockingQueue | 內建阻塞、流控、毒丸關閉 |
| 有序 Map、範圍查詢 | ConcurrentSkipListMap | 跳表實現,併發有序操作 |
| 集合視圖、Set | ConcurrentHashMap.newKeySet() | 基於 CHM,支援並發 Set 操作 |
避坑指南
- 不可用 putIfAbsent 替代 get-then-put 邏輯:語義不同,前者僅 Key 不存在
- size() 非精確值:併發環境下僅供參考,不可用於精確業務判斷
- 複合操作不可嵌套:
map.computeIfAbsent(k, v -> map.get(k2)) 可能死鎖
- Null 不支援:Key/Value 不可為 null,用特殊標記物件替代
- 迭代器弱一致性:不拋
ConcurrentModificationException,但可能讀到舊值