傳統方案對比:
| 方案 | 機制 | 缺點 |
|---|---|---|
| Hashtable | 全表鎖 (synchronized 方法) | 併發度極低,單線程吞吐 |
| Collections.synchronizedMap | 全表鎖 (包裝器) | 同 Hashtable |
| ConcurrentHashMap (JDK 7) | 分段鎖 (Segment 數組) | 擴容需鎖全表、Segment 固定 16 擴容困難 |
| ConcurrentHashMap (JDK 8+) | CAS + synchronized (節點首部) + 長鏈樹化 | 仍有鎖開銷、擴容協調複雜 |
無鎖目標:零阻塞、零上下文切換、可線性擴展。核心難點在於擴容時的多指針原子更新。
CPU 指令級原子操作:CAS(addr, expected, new) → 若 `*addr == expected` 則 `*addr = new` 返回 true,否則 false。
Java 暴露:`AtomicReference
// 經典 CAS 循環模式
AtomicInteger counter = new AtomicInteger(0);
public void increment() {
for (;;) {
int old = counter.get();
int newVal = old + 1;
if (counter.compareAndSet(old, newVal)) return;
// 失敗自旋重試
}
}
鏈表節點:
class Node {
final K key;
volatile V value; // 值可更新
final int hash;
final AtomicReference> next = new AtomicReference<>(); // CAS 修改 next
}
pred 和後繼節點 succ (按 hash 有序)。newNode,newNode.next.set(succ)。pred.next.compareAndSet(succ, newNode)。target 及其前驅 pred、後繼 succ。pred.next.compareAndSet(target, succ)。關鍵:僅修改 一個 `next` 指針,CAS 即可保證原子性。
擴容需將舊桶節點遷移到新桶:
// 需同時原子更新三個指針!
predOld.next = succOld // 從舊鏈移除
predNew.next = node // 插入新鏈
node.next = succNew
CAS 只能原子更新單個內存地址,無法同時驗證更新三個指針 → 無法直接無鎖遷移。
核心洞見:如果節點在鏈表中永遠按 hash 逆序排列,擴容時無需移動節點,只需新增桶頭哨兵節點!
定義 revHash = Integer.reverse(hashCode) | 1 (最高位置 1 區分正常節點與哨兵)。
桶索引 = revHash & (size - 1) (size 為 2 的冪)。
隨著 size 加倍 (2^i → 2^(i+1)),索引只增加一位二進制位,節點相對順序完全不變。
每個桶頭設置一個哨兵節點,`hash = Integer.reverse(bucketIndex)`,最低位為 0。
正常節點最低位為 1 → 哨兵永遠排在組首。
組內節點按 `revHash` 遞增有序。
size 翻倍時,原桶 `j` 分裂為 `j` 和 `j + oldSize` 兩組。
無需移動任何節點,只需在新桶位置創建新哨兵節點,並插入到原鏈表中正確位置 (即原哨兵與第一個屬於新組的節點之間)。
原鏈表節點的 `next` 指針完全不變!
class LockFreeHashMap {
// 桶數組:AtomicReference 數組,支持 CAS 更新桶頭
private final AtomicReference>[] buckets;
// 當前使用的桶數量 (2 的冪),擴容時 CAS 更新
private final AtomicInteger size = new AtomicInteger(2); // 初始 2
// 總節點數 (不含哨兵)
private final LongAdder count = new LongAdder();
// 負載因子閾值
private static final double LOAD_FACTOR = 0.75;
}
private Node initBucket(int bucketIdx) {
// 父桶索引:去掉最高位
int parentIdx = bucketIdx ^ Integer.highestOneBit(bucketIdx);
Node parentHead = buckets[parentIdx].get();
if (parentHead == null) {
parentHead = initBucket(parentIdx); // 遞歸初始化父桶
}
// 創建哨兵
Node sentinel = new Node<>(null, null, Integer.reverse(bucketIdx) & ~1);
// 插入到父鏈表正確位置 (CAS 循環)
return insertIntoList(parentHead, sentinel);
}
public V get(K key) {
int h = spread(key.hashCode());
int idx = h & (size.get() - 1);
Node head = buckets[idx].get();
if (head == null) return null; // 桶未初始化
int revHash = Integer.reverse(h) | 1;
for (Node p = head.next.get(); p != null; p = p.next.get()) {
if (p.hash == revHash && key.equals(p.key)) return p.value;
if (p.hash > revHash) break; // 有序優化:提前結束
}
return null;
}
public V put(K key, V value) {
int h = spread(key.hashCode());
int revHash = Integer.reverse(h) | 1;
for (;;) {
int curSize = size.get();
int idx = revHash & (curSize - 1);
// 懶惰初始化桶
Node head = buckets[idx].get();
if (head == null) {
initBucket(idx);
continue; // 重試
}
// 在鏈表中查找/插入
Node pred = head;
Node curr = pred.next.get();
while (curr != null && curr.hash < revHash) {
pred = curr;
curr = curr.next.get();
}
if (curr != null && curr.hash == revHash && key.equals(curr.key)) {
// 更新值 (CAS)
V oldVal = curr.value;
if (curr.value == oldVal || UNSAFE.compareAndSwapObject(curr, valueOffset, oldVal, value)) {
return oldVal;
}
continue; // CAS 失敗重試
}
// 插入新節點
Node newNode = new Node<>(key, value, revHash);
newNode.next.set(curr);
if (pred.next.compareAndSet(curr, newNode)) {
count.increment();
// 擴容檢查
if (count.sum() > curSize * LOAD_FACTOR) {
tryResize(curSize);
}
return null;
}
// CAS 失敗重試
}
}
private void tryResize(int oldSize) {
// 嘗試 CAS 更新 size (只有成功者執行擴容)
if (size.compareAndSet(oldSize, oldSize << 1)) {
int newSize = oldSize << 1;
// 為新桶創建哨兵 (並行可由多線程協作完成)
for (int i = oldSize; i < newSize; i++) {
initBucket(i); // 失敗不影響,下次訪問會重試
}
}
// 失敗者不執行擴容,繼續正常操作 (新 size 已生效)
}
| 維度 | 無鎖 HashMap (理論) | CHM (JDK 8+) |
|---|---|---|
| 同步原語 | 純 CAS (無鎖) | CAS + synchronized (節點首部) |
| 擴容方式 | 有序鏈表 + 哨兵分裂 (不移動節點) | 多線程協作遷移節點 (ForwardingNode) |
| Get 性能 | 極高 (單次 CAS 讀) | 極高 (volatile 讀,無鎖) |
| Put 性能 (高併發) | 理論最優 (無阻塞) | 優 (細粒度鎖,低衝突) |
| 實現複雜度 | 極高 (邊界條件多、ABA 問題、內存回收) | 高 (但已經過大規模生產驗證) |
| 內存回收 | 需 Hazard Pointer / Epoch GC | 依賴 JVM GC |
無鎖 HashMap 是並發數據結構的「珠穆朗瑪峰」:CAS 單指針原子性 + 有序鏈表消除多指針移動 + 懶惰分塊初始化 + 哨兵節點分組。理解其設計精髓——將「移動節點」轉化為「插入哨兵」,是掌握無鎖編程精髓的關鍵。雖然生產環境首選 `ConcurrentHashMap`,但研究無鎖 HashMap 能深度理解 CAS、內存模型、無鎖同步、擴容協調等核心難題,為設計高性能無鎖隊列、跳表、B+樹打下堅實基礎。