淘寶工程師曾曝光:將單線程 HashMap 改為多線程環境後,CPU 飆升 100%,排查發現是 HashMap 並發 put 導致鏈表形成環狀結構,get 陷入死循環。本文從 JDK 源碼層面深度剖析 Rehash 擴容機制、多線程競態條件、死循環形成全過程,並給出生產環境選型建議。
// 線程不安全的 HashMap
Map<String, Object> map = new HashMap<>();
// 多線程並發 put
new Thread(() -> map.put("key", "value")).start();
new Thread(() -> map.put("key2", "value2")).start();
// 後果:CPU 100%,get() 永不返回
許多開發者誤以為「懂 synchronized、volatile、JUC 就能寫對併發代碼」,實則線程安全極其反直覺,微小疏忽即釀成大禍。
// 看似安全的 CopyOnWriteArrayList / Collections.synchronizedList
if (list.size() > 0) { // 線程 A 檢查 size=1
return list.get(0); // 線程 B 此時 remove(0) → IndexOutOfBoundsException
}
教訓:單個操作線程安全 ≠ 復合操作線程安全。需額外加鎖或使用原子變量。
「發送郵件」看似獨立,實則涉及 Session、Transport 共享狀態,並發發送導致郵件內容錯亂、發送失敗。場景建模錯誤 是併發 Bug 溫床。
size > threshold (capacity * loadFactor)new Table[newCap] → 遍歷舊桶 → 重新計算 hash → 遷移節點到新桶// JDK 7 transfer() 核心遷移邏輯 (簡化)
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
while (e != null) {
Entry<K,V> next = e.next; // 保存下一節點
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 關鍵:頭插法
newTable[i] = e;
e = next;
}
}
}
場景:初始容量 2,負載因子 0.75,插入 3 個鍵觸發擴容。鏈表:A → B(同桶)。線程 1、2 並發 transfer。
Entry<K,V> next = e.next;(e=A, next=B),時間片用盡。transfer,完成遷移:新桶中 B → A(頭插法導致順序反轉)。e.next = newTable[i](此時 newTable[i] 已是 B),newTable[i] = e(A)。e = next(即 B),再次頭插:B.next = A,newTable[i] = B。A.next = B 且 B.next = A → 環形鏈表誕生!最終結構:A ↔ B (循環)
get(key) → while (e != null) e = e.next → 無限循環 → CPU 100%
JDK 7 為了性能,遷移時採用 頭插法,導致鏈表順序反轉。多線程並發遷移同一鏈表時,反轉邏輯交織產生環。JDK 8 改為 尾插法(保持原序),緩解但未根治:並發遷移仍可能導致節點丟失、數據覆蓋,本質上 HashMap 非線程安全。
Sun/Oracle 官方立場:HashMap 本來就不支持並發。要並發請用 ConcurrentHashMap。
鐵律:
HashMap、ArrayList 等非線程安全集合。ConcurrentHashMap(JDK 8 CAS + synchronized + 紅黑樹,分段鎖升級為 Node 級鎖)。CopyOnWriteMap / CopyOnWriteArrayList。LongAdder、AtomicReference 等原子類。Collections.synchronizedMap(new HashMap()) 僅保證單方法原子,復合操作仍需外部加鎖,性能差於 ConcurrentHashMap。| 版本 | 核心機制 | 關鍵點 |
|---|---|---|
| JDK 7 | 分段鎖 | 16 個 Segment 繼承 ReentrantLock,並發度 16 |
| JDK 8 | CAS + synchronized (Node) + 紅黑樹 | 鎖粒度細化到桶首節點,擴容支持多線程協助 |
| JDK 17+ | 優化擴容、CounterCell 改進 | 減少自旋、改進 size() 精度、增強穩定性 |
HashMap 死循環是非線程安全數據結構在並發擴容時頭插法導致鏈表成環的經典案例。它警示我們: