Java HashMap 多线程死循环核心原因是JDK 1.7扩容时头插法+竞态条件导致环形链表(如A↔B),引发get/put无限遍历、CPU 100%且线程状态始终RUNNABLE;JDK 1.8改用尾插法避免成环,但仍非线程安全,生产环境应首选ConcurrentHashMap。

Java HashMap 在多线程环境下导致死循环,核心原因不是锁竞争,而是扩容时链表指针被多个线程交叉修改,形成环形结构。一旦链表成环(如 A→B→A),后续调用 get()、put()、size() 或 toString() 等需遍历链表的方法,就会陷入无限跳转,CPU 持续 100%,线程状态始终为 RUNNABLE。
JDK 1.7 头插法是死循环的直接推手
在 JDK 1.7 中,扩容通过 transfer() 方法完成,采用头插法迁移节点:
-
e.next = newTable[i]:让当前节点指向新桶的原头结点 -
newTable[i] = e:把当前节点设为新桶的新头结点
单线程下只是链表反转,无害;但多线程并发时,两个线程同时处理同一旧桶(例如链表 A→B→null),就可能产生竞态:
- 线程 T1 读取
e = A,缓存next = B,然后被挂起 - 线程 T2 完成迁移:先插 B,再插 A → 新桶变成
A→B,且B.next = A - T1 恢复执行:
A.next = newTable[i](此时newTable[i] == A)→A.next = A
结果就是 A→A 自环,或更常见的 A↔B 循环。
立即学习“Java免费学习笔记(深入)”;
JDK 1.8 虽改尾插法,仍不解决线程安全问题
JDK 1.8 将扩容改为尾插法,避免了链表成环,但HashMap 本身依然不是线程安全的:
- 并发
put()可能导致数据覆盖或丢失(例如两个线程同时发现桶为空,都写入新节点,后者覆盖前者) - 并发读写仍可能引发
ConcurrentModificationException,或因结构不一致导致逻辑异常 - 红黑树转换、resize 中的数组复制等操作,均无同步保障
如何快速识别是不是 HashMap 死循环
线上 CPU 飙高时,用以下组合命令定位:
- 执行
top -H -p <pid>找出 CPU 最高的线程 ID(十进制) - 将其转为十六进制 nid,再运行
jstack -l <pid> - 搜索该 nid 对应堆栈,若大量线程停在
HashMap.get()或HashMap.put()的链表遍历循环(如while (e != null) { e = e.next; }) - 且所有线程状态都是
RUNNABLE、无锁等待信息(区别于真实死锁的BLOCKED)
满足以上特征,基本可判定为 HashMap 扩容引发的环形链表问题。
真正可靠的解决方案只有三个
别依赖“加锁”或“只读”这种临时手段,生产环境必须明确选型:
- 读多写少场景:用
ConcurrentHashMap—— 分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8+),线程安全且性能好 - 写极少、强一致性要求高:用
Collections.synchronizedMap(new HashMap()),但注意迭代时仍需手动同步 - 纯只读初始化后不再变更:可用
Map.copyOf()(JDK 10+)或ImmutableMap.of()(Guava),彻底杜绝修改可能


















