是,jdk7中hashmap多线程扩容时因头插法与竞态条件导致链表成环,引发get/put无限遍历、cpu飙升100%且无异常;需通过jstack看是否卡在getentry或transfer,结合heap dump确认next互指环。

确认是否真发生了死循环,而不是卡顿或高CPU
死循环在 JDK7 HashMap 中的表现非常典型:某个线程在 get() 或 put() 时陷入无限遍历链表,CPU 占用飙到 100%,但 JVM 进程不崩溃、不抛异常、无日志输出。此时不能仅凭响应慢就下结论——可能是 GC 频繁、锁竞争或外部依赖阻塞。
验证方法很简单:jstack <pid></pid> 抓取线程快照,重点看是否有线程长期停留在 java.util.HashMap.getEntry(HashMap.java:...) 或 transfer(HashMap.java:...) 的栈帧里,且调用链中反复出现 e = e.next 类似逻辑。如果多个线程都卡在同一个 HashMap 实例的相同方法上,基本可以锁定。
定位触发死循环的具体扩容场景
JDK7 的死循环只发生在扩容(resize())过程中,且必须满足两个前提:多个线程同时触发扩容 + 扩容目标桶中存在长度 ≥2 的链表。单线程永远不会出问题,纯读操作也不会触发。
- 检查代码中是否存在共享的
HashMap实例被多个线程并发put() - 确认该
HashMap初始化时未指定足够容量,导致早期频繁扩容(例如默认 16 容量、负载因子 0.75 → 第 13 次put就触发第一次 resize) - 注意:即使只有一处
put(),若该 Map 被多个线程通过不同路径访问(如 Spring Bean 单例注入),仍算并发写
复现时为什么很难稳定触发?关键变量在哪
死循环不是必然发生,它依赖线程调度时序和内存可见性——这正是最易被忽略的复杂点。
核心条件有三个,缺一不可:
- 两个线程 T1、T2 同时进入
transfer(),处理同一段旧链表(比如 A→B→null) - T1 执行完 A 插入新表后被挂起,T2 完成整个链表迁移(变成 B→A→null)
- T1 恢复执行,用已失效的局部变量
e(仍指向 A)和next(仍为 B)继续操作,把 A 的next指向 B,而 B 的next原本就指向 A → 形成 A↔B 环
这意味着:哪怕你用 100 个线程并发 put,只要没有恰好卡在那个窗口期,就不会死循环。这也是为什么压测脚本常要跑几百上千次才能捕获一次。
别只盯着代码,先看 JVM 参数和运行时特征
很多团队花几天查代码逻辑,却漏掉了更直接的线索:
-
jdk7u80及之后版本在transfer()内部加了简单计数保护(非解决,仅降低概率),但不影响根本行为 - 如果应用使用 CMS 或 G1 垃圾回收器,且发生并发模式失败(concurrent mode failure),会加剧线程调度不确定性,反而更容易暴露该问题
- Arthas 的
thread -n 5可实时观察 CPU 占用 top5 线程,比反复jstack更快定位“正在死循环”的线程 - 一旦确认是
HashMap死循环,不要尝试修复——它不是 bug,是设计使然。立刻替换为ConcurrentHashMap或加显式同步,否则下次可能在生产凌晨三点爆发










