JDK1.7中HashMap扩容时因头插法+多线程并发导致环形链表,引发get等操作无限循环、CPU 100%;解决方案是改用ConcurrentHashMap或升级至JDK1.8(尾插法避免成环)。

HashMap 在 JDK 1.7 中的头插法是根源
JDK 1.7 的 HashMap 扩容采用头插法,即新节点总是插入链表头部。当多个线程同时触发扩容时,原本顺序为 A → B → null 的链表,可能被两个线程交替操作,最终形成 A → B → A 的环形结构。
一旦链表成环,后续任何 get() 或 containsKey() 操作都会陷入无限遍历——因为 next 指针永远不为 null,循环无法终止。CPU 就会持续执行这个无退出条件的 while 循环,使用率迅速拉满。
典型触发场景:多线程 put 引发并发扩容
以下情况极易触发该问题:
- 未加锁或未同步的全局 HashMap 被多个线程反复调用 put()
- 初始容量小(如默认 16),负载因子 0.75,插入第 13 个元素就可能触发首次扩容
- 多个线程几乎同时达到扩容阈值,各自开始自己的 resize() 流程
- 线程 A 执行到旧链表迁移中途挂起,线程 B 完成整个扩容并修改了 next 指针,A 恢复后继续错误链接
为什么 CPU 会卡死而不是报错或阻塞
这不是异常或阻塞,而是纯粹的计算型死循环:
- 线程状态在 jstack 中显示为 RUNNABLE,而非 BLOCKED 或 WAITING
- 没有 I/O、锁等待或 sleep,纯指针跳转 + 条件判断,指令极轻但永不退出
- 单个线程就能把一个 CPU 核心占满;若多个线程同时陷入各自环路,整机 CPU 可能飙至 100%
- GC 线程通常不受影响,所以 jstat -gc 可能显示 GC 正常,误导排查方向
解决方案与替代选择
根本原则:禁止在多线程环境中直接使用非线程安全的 HashMap。
- 高并发读写场景:改用 ConcurrentHashMap(JDK 1.7 使用分段锁,JDK 1.8 改为 CAS + synchronized 链表/红黑树)
- 仅读多写少:可用 Collections.synchronizedMap(new HashMap()),但注意迭代时仍需手动同步
- 初始化阶段确定大小且不再写入:可考虑 ImmutableMap(Guava)或 Map.copyOf()(JDK 10+)
- 升级 JDK:JDK 1.8 的 HashMap 扩容采用尾插法,虽仍不保证线程安全,但不会构造环形链表,最多数据丢失或覆盖










