JDK 1.7 HashMap 多线程扩容因头插法+无锁并发导致环形链表,引发 get/put 等操作无限循环、CPU 100%、线程状态为 RUNNABLE;JDK 1.8 改用尾插法并加锁优化,避免成环但仍未完全线程安全。

Java 中 HashMap 1.7 在多线程环境下因头插法扩容导致死循环,本质是两个线程交叉修改链表指针,破坏了单向性,最终形成 A→B→A 的闭环。这不是锁竞争问题,而是一个纯粹的竞态条件(race condition)引发的无限遍历。
头插法本身在单线程下就反转链表
HashMap 1.7 的 transfer() 方法迁移节点时,固定执行两步:
- e.next = newTable[i]:让当前节点 e 指向新数组该位置原来的头节点(初始为 null)
- newTable[i] = e:把 e 设为该位置新的头节点
比如旧链表是 A→B→C→null,按顺序迁移后,新链表变成 C→B→A→null —— 顺序完全翻转,但仍是合法单向链表,无害。
两个线程并发操作同一桶,指针被错乱覆盖
假设旧桶中只有两个节点:A→B→null,且 T1、T2 同时对该桶扩容:
- T1 开始迁移 A:读取
e = A,缓存next = B,执行A.next = newTable[i](此时 newTable[i] 是 null),再设newTable[i] = A;然后 T1 被挂起 - T2 接着完整执行:先迁移 B →
B.next = null变成B.next = A,newTable[i] = B;再迁移 A →A.next = B,newTable[i] = A;最终 newTable[i] 是 A→B→A→B… - T1 恢复:仍用旧缓存
next = B,执行A.next = newTable[i]→ 即A.next = A(因为此时 newTable[i] 已是 A);再执行newTable[i] = A
结果就是 A.next = A,或更常见的是 A↔B 相互指向,构成环。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
环一旦形成,get/put 就会无限循环
后续任何需要遍历该桶链表的操作都会卡住:
- get(key):进入 while(e != null) { e = e.next; },永远绕圈
- put(key, value):检查 key 是否已存在,需遍历链表,同样陷入循环
- size()、toString():全表扫描时只要碰到这个桶,就会停住
线程状态始终是 RUNNABLE,CPU 占用飙升到 100%,jstack 显示堆栈停在 HashMap.java 的链表遍历行,没有锁信息 —— 这正是环形链表的典型特征。
JDK 1.8 怎么解决这个问题
1.8 改用尾插法迁移,保证节点相对顺序不变,避免了指针反向覆盖;同时 resize 时对桶加了 synchronized 锁(基于 Node 的 final hash 字段做分段锁),进一步降低并发风险。但要注意:1.8 的 HashMap 仍不是线程安全容器,仅规避了死循环,不保证 put/get 的原子性与可见性。高并发场景应直接使用 ConcurrentHashMap。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










