java 7及之前hashmap多线程扩容时因头插法并发操作导致环形链表,引发get/put无限循环;java 8改用尾插法和红黑树规避该问题,推荐使用concurrenthashmap。

Java 7 及之前版本的 HashMap 在多线程环境下扩容(resize)时可能引发死锁,根本原因在于其 链表头插法 + 多线程并发修改 导致环形链表,进而使 get() 或 put() 操作陷入无限循环——严格来说这不是传统意义上的“死锁”(如 synchronized 锁循环等待),而是 CPU 飙高、线程假死,表现类似死锁。
为什么扩容过程会形成环形链表
Java 7 的 HashMap 扩容时,对每个桶(bucket)中的链表采用头插法重新散列到新数组。假设原数组长度为 2,两个线程 T1 和 T2 同时触发 resize,新数组长度为 4;若某桶中已有节点 A → B(A.next = B),在并发转移过程中:
- T1 读取 A,摘下 A,插入新桶头部;
- T2 也读取 A,摘下 A,插入新桶头部;
- T1 继续处理 B,摘下 B,头插到新桶 → 此时变成 B → A;
- T2 也处理 B,头插后变成 B → A → B(因 A 的 next 仍指向 B);
结果:链表成环(A ⇄ B),后续遍历该桶(如 get("key"))会无限循环。
什么操作会触发这个现象
必须同时满足以下条件:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 使用的是 Java 7 或更早的
HashMap(Java 8 已改为红黑树 + 尾插法,规避此问题); - 多个线程同时执行
put(),且触发扩容(即 size > threshold); - 多个线程操作的是同一个桶位置上的链表(哈希冲突较多时更易发生);
- 无任何同步控制(未加锁、未用
ConcurrentHashMap等线程安全结构)。
Java 8 为什么不再出现这个问题
Java 8 对 HashMap 做了关键改进:
- 扩容时改用尾插法,保证节点相对顺序,不会反转指针;
- 当链表长度 ≥ 8 且数组长度 ≥ 64 时,自动转为红黑树,避免长链表并发问题;
- 扩容过程虽仍非线程安全,但不会再构造环形结构,最多导致数据丢失或覆盖,不会无限循环。
如何避免和替代方案
生产环境绝不要让 HashMap 在多线程下被并发写入:
- 读多写少:用
ConcurrentHashMap(Java 7 分段锁 / Java 8 CAS + synchronized 优化); - 写极少且初始化后只读:用
Collections.unmodifiableMap()包装; - 需完全独占:手动加锁(如
synchronized(map)),但性能差,不推荐; - 明确线程封闭:确保每个线程使用独立的
HashMap实例(如 ThreadLocal)。
一句话总结:这不是 JVM 层面的锁死,而是代码逻辑缺陷导致的无限循环;升级到 Java 8+ 并用 ConcurrentHashMap 是最稳妥解法。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










