jdk 1.7 hashmap 多线程扩容会死循环,根本原因是 transfer() 方法采用无锁头插法:两线程并发迁移链表 a→b 时,t1 挂起后 t2 完成反转为 b→a,t1 恢复后用过期 next 指针操作,导致 a→b→a 环形链表,get() 遍历时无限循环;jdk 1.8 改用尾插法并优化迁移逻辑,从机制上避免成环,但 hashmap 仍非线程安全。

Java 中 HashMap 的死循环扩容问题,只存在于 JDK 1.7 及更早版本,JDK 1.8 已从机制上彻底解决该问题,但并未让 HashMap 变成线程安全容器。核心改进在于重写了扩容时的链表迁移逻辑。
为什么 JDK 1.7 会死循环
根本原因是 transfer() 方法使用无锁头插法迁移链表节点。当两个线程并发扩容同一条链表(如 A → B → null)时:
- 线程 T1 暂存
e = A、next = B,将 A 插入新数组头,此时新表位置为A → null; - T1 被挂起,T2 完整执行:把 B 插到同一位置头,再把 A 插到 B 前面,结果变成
B → A → null; - T1 恢复后,仍用旧视角继续处理:取
e = B,发现next = A(已被 T2 修改),再将 B 插到新表头,覆盖 A 的 next,最终形成A → B → A的环。
后续对该桶的 get() 或 containsKey() 就会在遍历中无限循环。
JDK 1.8 是怎么解决的
关键改动有两点:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
- 改头插为尾插:迁移时保持原链表顺序,新节点总是追加到链表尾部,不会反转或交叉修改 next 指针;
-
引入红黑树与位运算迁移优化:扩容时不再逐个 rehash 计算下标,而是利用“容量为 2 的幂”特性,通过
(e.hash & oldCap)判断节点在新数组中是留在原下标,还是移到原下标 + oldCap,大幅减少哈希计算,并天然避免链表结构被破坏。
即使多线程同时扩容,最多导致数据丢失或 size 不准,但链表不会成环,get 操作不会死循环。
但 HashMap 依然不是线程安全的
JDK 1.8 解决了死循环,不代表能用于并发写场景。以下问题仍然存在:
- 多个线程同时
put同一个 key,可能因竞态导致后写覆盖前写; -
size字段非原子更新,高并发下统计值严重不准; - 扩容过程虽不环形,但若未同步,仍可能部分线程读到旧数组、部分读到新数组,引发不一致状态。
生产环境正确做法
不要试图“修复” HashMap 的并发问题,而应选择合适工具:
- 首选 ConcurrentHashMap:分段锁(JDK 7)或 CAS + synchronized(JDK 8+)保障线程安全,性能远高于加锁的 HashMap;
- 读多写少且并发低时,可考虑
Collections.synchronizedMap(new HashMap()),但所有操作串行化,吞吐受限; - 绝对避免在共享上下文中直接使用裸 HashMap 做并发 put —— 即使是 JDK 1.8,这也是隐患源头。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










