Java 7 HashMap 多线程扩容时因头插法和非原子操作易形成环形链表,导致无限循环;JDK 8 改用尾插法、红黑树及高位拆分规避该问题;推荐使用 ConcurrentHashMap。

Java 7 及更早版本的 HashMap 在多线程环境下执行 put 并触发扩容(resize)时,可能因头插法 + 非原子操作 + 线程交错执行,导致链表节点的 next 指针被错误重写,最终形成环形结构。一旦成环,后续 get、size 或遍历操作就会陷入无限循环,CPU 占用飙升至 100%。
关键机制:头插法迁移是根源
在 JDK 7 中,resize 过程通过 transfer 方法将旧数组中每个桶的链表逐个迁移到新数组。迁移采用头插法:对原链表从头到尾遍历,每次取出一个节点 e,将其插入到新桶链表的最前面,即:
-
e.next = newTable[i]—— 让当前节点指向新桶当前的头节点 -
newTable[i] = e—— 把当前节点设为新桶新的头节点 -
e = next—— 继续处理下一个节点
这个逻辑在单线程下完全正确,但在多线程并发迁移同一桶时,两个线程共享同一个旧链表(如 a → b → null),却各自操作独立的 newTable(局部变量),问题出在对原链表节点引用关系的并发修改上。
典型成环过程(两线程迁移同一桶)
假设旧桶链表为 a → b → null,线程 A 和线程 B 同时开始迁移该桶:
- 线程 A 先取出
a,执行a.next = newTable[i](此时newTable[i]为null),再执行newTable[i] = a,此时新桶为a → null,A 的e变为b - 此时线程 B 插入执行:它读取旧链表头仍是
a,执行a.next = newTable[i]—— 注意,此刻newTable[i]已被线程 A 设为a,所以a.next被设为a,即a → a - 接着 B 执行
newTable[i] = a(无实质变化),再继续处理b;而 A 后续又将b插入到a前面,得到b → a → a - 最终结果:节点
a的next指向自己,或a → b → a形成闭环
JDK 8 如何规避该问题
JDK 8 未修复“线程安全”,但彻底消除了死循环风险:
-
迁移改用尾插法:新节点总是追加到链表尾部,不再反转顺序,避免了
next指针被反复覆盖 -
引入红黑树优化:当链表长度 ≥ 8 且数组长度 ≥ 64 时转为红黑树,树结构天然不依赖
next链式遍历 - 扩容逻辑重构:迁移过程按高低位拆分,每个线程处理不同子集,减少竞争(但仍不保证 put 原子性)
真正安全的替代方案
若需多线程写入,不应依赖任何“侥幸不触发扩容”的 HashMap 使用方式:
- 用
ConcurrentHashMap:JDK 7 使用分段锁,JDK 8 改为 CAS + synchronized 控制桶级粒度,支持高并发读写 - 外部同步:如
Collections.synchronizedMap(new HashMap()),但会严重降低并发性能 - 初始化足够大 + 禁用扩容:仅适用于写少读多、容量可预估的极少数场景,不推荐
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











