jdk 7 hashmap并发扩容时因头插法导致环形链表,引发get()无限遍历、cpu 100%;jdk 8改用尾插法+高低位拆分,消除成环可能,但仍未实现线程安全。

Java HashMap 在 JDK 7 中因头插法 + 并发扩容,确实可能引发死循环,这不是偶然 bug,而是结构性风险。
头插法本身在单线程下完全正常
JDK 7 的 Entry 链表插入始终用头插:新节点直接成为链表首节点,原头节点变成它的 next。例如插入顺序 A→B→C,链表实际存储为 C→B→A(倒序)。这不影响功能,只是顺序不同。
- put 操作中调用
addEntry(),核心是table[i] = new Entry(hash, key, value, e),即“新节点指向旧头,再把新节点设为新头” - 扩容时的
transfer()方法也沿用该逻辑:对每个旧桶遍历链表,逐个头插到新桶
并发扩容时,两个线程对同一链表操作会破坏指针结构
死循环不是发生在插入时,而是在多线程同时触发 resize(),且恰好对同一个哈希桶(比如 table[3])执行数据迁移时发生的。
- 假设原链表为 A→B→C→null,两线程 T1 和 T2 同时开始迁移该链表
- T1 先执行:读取 e=A,next=B;头插 A 到新桶 → 新桶头为 A;再读 e=next=B,next=C;头插 B → 新桶变为 B→A;继续头插 C → 最终为 C→B→A
- T2 在 T1 执行中途被挂起,它仍持有旧状态:e=A,next=B;等它恢复时,新桶已被 T1 改为 C→B→A,此时 B 的
next已指向 A(因 T1 头插 B 时设了B.next = A) - T2 接着执行:头插 A →
A.next = 当前新桶头(即 C),然后设新桶头为 A;再处理 e=next=B → 头插 B,B.next = A,新桶头变 B;此时链表变成 B→A→C→B… 实际形成 A↔B 相互引用的环
get() 方法陷入无限遍历
一旦链表成环(如 A→B→A→B…),任何遍历该桶的操作都会卡住:
-
get()内部循环是for (Entry e = table[i]; e != null; e = e.next) - 当
e走到 A,e.next是 B;走到 B,e.next又是 A;永远不满足e == null - CPU 占用飙升至 100%,线程堆栈停留在
HashMap.get()的 for 循环里,服务无响应但不报错
JDK 8 是如何避免的
不是靠加锁,而是重构迁移逻辑:
- 改用尾插法:迁移时保持原链表顺序,A→B→C 迁移后仍是 A→B→C,不会反转,自然无法形成环
- 引入红黑树:链表长度 ≥8 且数组长度 ≥64 时转为树结构,进一步降低长链表风险
- 注意:JDK 8 的 HashMap 仍非线程安全——它只修复了死循环,但并发 put 仍可能导致数据覆盖或丢失
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











