JDK 1.7中HashMap死循环由多线程并发扩容时头插法导致链表成环引发;JDK 1.8改用尾插法从机制上消除该问题,但HashMap仍非线程安全,高并发场景应使用ConcurrentHashMap。

Java HashMap 的死循环问题主要出现在 JDK 1.7 及更早版本中,本质是多线程并发扩容时头插法 + 无同步保护导致链表成环;JDK 1.8 虽改用尾插法消除了环形链表风险,但 HashMap 本身仍不支持并发写。问题不常发生,但一旦触发,后果严重:CPU 拉满、接口卡死、无异常日志、jstack 显示线程停在 get() 或 put() 的遍历循环里。
死循环怎么被触发
必须同时满足以下三个条件:
- 多线程同时执行 put 操作,且至少有两个线程触发了扩容(即 size ≥ threshold)
- 哈希冲突集中于同一桶,比如多个 key 经 hash 后落在 table[3],形成 A→B→C 链表
- 两个线程在 resize() 过程中交叉执行头插迁移:T1 插入 A、B、C 到新桶,T2 在中途恢复并复用旧的 next 引用,把 B.next 设为 A、A.next 设为 B,最终形成 A↔B 环
典型复现场景:初始化容量极小(如 new HashMap(2))、高并发写入、key 哈希值高度集中(如大量 null key 或相同字符串)。
为什么只在 JDK 1.7 出现
JDK 1.7 的 transfer() 方法采用头插法迁移链表节点,且全程无任何锁或 volatile 保护。节点插入顺序是反向的(C→B→A),而多线程交错读写 e 和 e.next 会破坏引用关系。JDK 1.8 改为尾插法,迁移后链表顺序与原链一致(A→B→C),即使并发也不会反转指针,从根本上避免了环的产生。但注意:ConcurrentHashMap 才是真正线程安全的替代品,HashMap 在 1.8 中依然禁止多线程写。
立即学习“Java免费学习笔记(深入)”;
如何避免死循环
核心原则:不让 HashMap 承担并发写的职责。
-
读多写少场景:用
Collections.synchronizedMap(new HashMap()),适合写操作极少、且能接受全局锁粒度的场景 -
高并发读写场景:直接使用
ConcurrentHashMap,它通过分段锁(1.7)或 CAS + synchronized(1.8+)保证线程安全,resize 也支持并发迁移 -
写操作可串行化:用
ReentrantLock或synchronized包裹所有 put/get 操作,但会显著降低吞吐量,仅建议临时兜底 -
启动期预热规避:设置合理初始容量和负载因子(如
new HashMap(1024, 0.75f)),减少运行时扩容概率
线上排查关键信号
发现服务响应变慢、CPU 持续 100% 但 GC 正常时,立即抓取线程快照:
- 执行
jstack <pid> > jstack.log</pid>,搜索HashMap.get或getEntry - 若多个 RUNNABLE 线程堆栈都卡在
for (Entry e = table[i]; e != null; e = e.next),基本可判定为死循环 - 配合
top -Hp <pid></pid>定位高 CPU 线程 ID,再查其具体调用栈
没有报错、没有 OOM、没有死锁提示——这是 HashMap 死循环最典型的“静默崩溃”特征。


















