一句话结论:
在 多线程并发环境下 JDK1.7 的 HashMap 扩容时,由于链表转移过程中不是线程安全的,可能会形成环形链表结构,从而导致遍历时无限循环,占满 CPU。
📌 一、背景知识:HashMap 扩容机制(JDK 1.7)
在 HashMap 中,数组默认长度为 16,负载因子为 0.75。插入数据超过容量时会触发扩容(resize),比如插入第 13 个元素时:
threshold = capacity * loadFactor = 16 * 0.75 = 12
扩容做了什么?
-
数组容量扩大为原来的两倍
-
将原数组中的元素重新分配位置(Rehash)
-
转移链表节点到新数组的对应桶中
🧩 二、问题出在:链表转移时可能顺序被打乱
在 JDK 1.7 中,链表是头插法(先插入的元素在后面,新的元素在前面),举例:
Entry<K, V> newEntry = new Entry<>(k, v, oldEntry);
多个线程同时执行扩容时,会同时修改同一个桶对应的链表结构。
🔥 三、并发条件下形成“闭环链表”的可能过程(关键)
假设两个线程 Thread A 和 Thread B 同时进行 resize() 操作。
示例场景:
原来一个桶的链表如下(顺序为 e1 → e2 → e3):
Thread A 复制时:
e1.next = e2
e2.next = e3
e3.next = null
Thread A 把链表放入新数组的时候,做的是头插法,插入顺序被颠倒:
newBucket = e3 → e2 → e1
但此时 Thread B 也在操作这段链表,而两线程交叉执行,就有可能导致:
e1.next = e2
e2.next = e1(成环!)
链表变成:
e1 → e2 → e1 → e2 → e1 → ...(死循环)
🧠 四、为什么这个问题只发生在 JDK 1.7?
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 链表插入方式 | 头插法 | 尾插法 |
| resize 机制 | 多线程可同时触发 | resize 加了 synchronized |
| 并发 resize 安全性 | ❌ 不安全 | ✅ 安全(同步+新链表结构) |
因此,在 JDK 1.8 中已经用 synchronized 保证 resize 线程安全,不会出现链表成环。
🎯 五、面试回答模板(可直接背诵)
在 JDK 1.7 中,HashMap 的扩容采用的是头插法,多个线程并发执行 resize 操作时,如果同时迁移同一个桶的链表,会因为链表插入顺序错乱,造成链表形成闭环,从而在遍历时产生死循环。这种问题在 JDK 1.8 中已通过加锁和链表尾插法修复,避免了并发 resize 的不一致问题。
✅ 五、结论归纳
| 问题 | 说明 |
|---|---|
| 触发时机 | 多线程并发 put 触发 resize |
| 死循环原因 | 链表迁移过程中,头插法打乱链表顺序,导致链表成环 |
| 版本限制 | 仅发生在 JDK 1.7 或之前 |
| 修复方法 | JDK 1.8 之后使用尾插法 + synchronized 加锁 |
🧩六、JDK 1.7 vs JDK 1.8 的 ConcurrentHashMap 实现原理对比
| 版本 | 底层结构 | 锁机制 | 特点 |
|---|---|---|---|
| JDK 1.7 | Segment数组 + HashEntry数组 | 分段锁(Segment 继承 ReentrantLock) | 每段加锁,提高并发 |
| JDK 1.8 | 数组 + 链表 + 红黑树 | CAS + synchronized | 不再使用 Segment,精度更细,性能更高 |
🚀 七、面试高频问题与答题模板
Q1:为什么不用 HashMap?
答:HashMap 在多线程环境下可能出现数据不一致、数据丢失、甚至链表死循环问题(如 JDK1.7 扩容时),不适合并发场景。
Q2:ConcurrentHashMap 如何实现线程安全?
在 JDK1.7 中使用了 Segment 分段锁机制,通过分段数组的粒度来提高并发性;
在 JDK1.8 中取消了 Segment,使用了CAS + synchronized + volatile 的混合机制,对单个桶节点加锁或无锁,避免整体锁,提高了并发性能。
Q3:ConcurrentHashMap 会不会出现死循环?
答:不会。JDK1.8 的链表插入使用尾插法,并且加锁控制了 put 和扩容过程,避免了成环死循环的问题。
🧠 面试说法(你可以背下来)
HashMap 是线程不安全的,特别是在高并发情况下会出现数据丢失甚至死循环的问题。JDK1.8 的 ConcurrentHashMap 用 CAS + synchronized 解决了并发写入冲突问题,get 是无锁的,put 时只锁住当前桶。扩容时还支持多线程协作迁移,避免全表阻塞,非常适合多线程读写共享的场景。
1335

被折叠的 条评论
为什么被折叠?



