HashMap并发死循环本质是JDK 1.7头插法扩容时多线程交叉修改next指针导致链表成环(如A→B→A),遍历时无限循环使CPU达100%且线程状态为RUNNABLE。

Java 中 HashMap 并发死循环本质是链表成环,不是锁竞争导致的死锁,而是多线程同时触发扩容时,对链表节点 next 指针的交叉修改破坏了结构,最终形成 A→B→A 这类闭环。一旦有线程调用 get()、put() 或 size(),就会在遍历桶内链表时无限跳转,CPU 占用飙升至 100%,线程状态始终为 RUNNABLE。
死循环产生的关键条件
这个现象集中在 JDK 1.7 及更早版本,核心触发路径如下:
-
扩容时机重合:多个线程几乎同时检测到元素数量超过
threshold(容量 × 加载因子),都进入resize()流程 -
头插法迁移:JDK 1.7 的
transfer()使用头插法——每次把旧链表节点e插入新桶头部,操作是两步:
①e.next = newTable[i](让当前节点指向新桶原头结点)
②newTable[i] = e(把当前节点设为新桶新头结点) -
线程调度中断:线程 T1 读取节点 A,缓存
A.next = B,执行第①步前被挂起;T2 完成整个链表(A→B→C)迁移,此时新桶中顺序为 C→B→A,且B.next = A,A.next = null -
恢复后错误续写:T1 恢复,仍用旧缓存的
B作为 next,执行A.next = newTable[i]→ 实际变成A.next = A(因为此时newTable[i]已是 A),或与 T2 状态叠加形成 A↔B 循环
JDK 版本差异决定风险等级
不同 JDK 对该问题的处理方式直接影响是否可能成环:
- JDK 1.7:明确使用头插法,无任何并发保护,死循环高发,是经典面试题来源
-
JDK 1.8:改用尾插法迁移链表,单次迁移不再反转顺序,避免了成环逻辑;但
HashMap本身仍非线程安全,多线程put仍可能导致数据丢失、覆盖或ConcurrentModificationException - 无论哪个版本:都不应假设其能承受并发写,仅靠升级 JDK 不能替代正确的并发设计
真正有效的规避方式
不依赖“小心使用”或“低概率侥幸”,而应从架构和选型层面切断风险:
立即学习“Java免费学习笔记(深入)”;
- 首选 ConcurrentHashMap:它通过分段锁(JDK 1.7)或 CAS + synchronized 桶锁(JDK 1.8+)保证扩容和写操作原子性,完全规避链表成环可能,且性能远超全局锁方案
- 读多写少场景可考虑 Collections.synchronizedMap():对整个 map 加 synchronized,简单但吞吐量低,适合初始化后只读、偶发写入的配置类场景
- 必须用 HashMap 时,严格隔离线程访问:例如每个线程独享一个实例,或由单一线程负责所有写操作(如通过队列投递变更),其他线程只读快照
-
禁用任何共享可变 HashMap 的“手动加锁”尝试:比如在业务代码里对
put块加synchronized(this),容易遗漏get、size、迭代等方法,且无法覆盖内部扩容逻辑,徒增复杂度却无效
线上排查死循环的信号特征
遇到疑似问题时,快速确认是否为 HashMap 成环,比盲目加锁更高效:
- 执行
jstack -l <pid>,发现多个线程堆栈停在HashMap.get(HashMap.java:xxx)或HashMap.put(HashMap.java:xxx),行号通常落在while (e != null) { e = e.next; }类似循环体 - 线程状态全为 RUNNABLE,无
WAITING、BLOCKED,也无锁持有信息(区别于真实死锁) - 配合
top -H -p <pid>查 CPU 最高线程,再用 jstack 中的 nid(十六进制)定位其完整堆栈,大概率看到上述链表遍历卡点


















