news 2026/5/30 17:00:57

腾讯Java面试被问:HashMap在并发下具体怎么死循环的?1.8真的解决了吗?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
腾讯Java面试被问:HashMap在并发下具体怎么死循环的?1.8真的解决了吗?

HashMap 并发死循环问题深度解析

一、问题的核心:JDK 1.7 的链表情形

1. 1.7 HashMap 扩容机制回顾

java

// JDK 1.7 HashMap 扩容关键代码 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; // 遍历旧数组 for (Entry<K,V> e : table) { while(null != e) { // 遍历链表 Entry<K,V> next = e.next; // ⚠️ 关键:暂存下一个节点 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); // 计算新位置 // ⚠️ 头插法:新节点插入链表头部 e.next = newTable[i]; // 1. 当前节点指向新表对应位置的第一个节点 newTable[i] = e; // 2. 将当前节点设为新表对应位置的头节点 e = next; // 继续处理下一个节点 } } }

二、死循环发生的详细过程

场景设定

假设:

  • 初始 HashMap:容量为 2,扩容阈值为 1.5

  • 已有节点:A -> B -> null(A 在 table[0],A.next = B)

  • 两个线程 T1、T2 同时执行put操作触发扩容

步骤拆解:头插法的致命缺陷

初始状态

text

旧表 table[0]: A → B → null

两个线程都开始扩容,各自创建新数组newTable(容量为4)。

线程 T1 执行到一半被挂起

java

// T1 执行状态: Entry<K,V> next = e.next; // e = A, next = B e.next = newTable[i]; // A.next = null(新表该位置为空) newTable[i] = e; // newTable[0] = A e = next; // e = B // T1 在此处被挂起!此时T1的视角: // newTable[0]: A → null // 本地变量:e = B, next = B(实际上B.next仍指向null)
线程 T2 完整执行完扩容

T2 不受干扰地完成了整个扩容过程:

text

// T2 完整执行后的结果: 新表 newTable[0]: B → A → null // 注意:因为头插法,顺序变为 B→A

此时内存中实际链表结构:

text

B.next = A A.next = null
T1 恢复执行(灾难开始)

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案

【点击此处即可/免费获取】​​​

T1 恢复时,它不知道链表已被 T2 修改,仍基于自己挂起前的状态继续:

java

// T1 恢复时状态: // e = B(从挂起处继续) // 但此时 B.next 已被 T2 改为 A(而不是 T1 之前看到的 null) Entry<K,V> next = e.next; // e = B, next = A(❗关键变化!) e.next = newTable[i]; // B.next = newTable[0] = A // 现在:B.next = A newTable[i] = e; // newTable[0] = B e = next; // e = A // 第一轮循环结束,此时: // newTable[0]: B → A(A又是B.next,形成 B↔A 互指)

继续下一轮循环:

java

// 第二轮循环开始:e = A Entry<K,V> next = e.next; // e = A, next = null(A.next是null) e.next = newTable[i]; // A.next = newTable[0] = B // 现在:A.next = B newTable[i] = e; // newTable[0] = A e = next; // e = null // 此时链表结构: // newTable[0]: A → B → A → B → ... 无限循环 // B.next = A, A.next = B
最终形成的环形链表

text

┌─────┐ │ ↓ newTable[0]: A → B ↑ │ └─────┘

查询时:get(key)若命中 table[0] 位置,将陷入A→B→A→B...的死循环。


三、可视化死循环形成过程

时间线演示

text

时间点 | 线程T1 | 线程T2 | 实际链表结构 ------|-------------------|-------------------|------------------- t0 | 开始扩容 | 开始扩容 | A→B→null t1 | 处理A: A→null | | A→null, B独立 t2 | 挂起(e=B) | 继续执行 | - t3 | | 处理A: A→null | T2的newTable: A→null t4 | | 处理B: B→A→null | T2的newTable: B→A→null t5 | 恢复(e=B) | 完成 | 实际: B→A→null t6 | next=A(B.next=A) | | - t7 | B.next=newTable[0]| | B.next=A t8 | newTable[0]=B | | newTable: B↔A互指 t9 | e=A | | - t10 | next=null(A.next) | | - t11 | A.next=B | | 形成环: A→B→A...

关键问题根源

  1. 头插法反转顺序:新旧链表顺序相反

  2. 共享状态无保护:两个线程操作同一链表

  3. 中间状态暴露enext的临时变量被另一个线程修改


四、JDK 1.8 真的解决了吗?

1. 1.8 的重大改进

java

// JDK 1.8 HashMap 扩容关键代码(简化) final Node<K,V>[] resize() { // ... 创建新数组 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // ⭐ 关键1:清空旧桶,避免多个线程操作同一链表 if (e.next == null) // 单节点直接迁移 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 红黑树处理 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // ⭐ 关键2:保持顺序的尾插法 Node<K,V> loHead = null, loTail = null; // 低位链表头尾 Node<K,V> hiHead = null, hiTail = null; // 高位链表头尾 do { Node<K,V> next = e.next; // 判断节点在新表的位置(原位置或原位置+oldCap) if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; // ⭐ 尾插法 loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; // ⭐ 尾插法 hiTail = e; } } while ((e = next) != null); // 将链表放入新表 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

2. 1.8 如何"解决"死循环

不是完全免疫,而是大幅降低概率:

改进点1:尾插法保持顺序

java

// 1.7 头插法(导致反转): e.next = newTable[i]; // 新节点指向头 newTable[i] = e; // 新节点成为头 // 1.8 尾插法(保持顺序): if (tail == null) head = e; // 第一个节点 else tail.next = e; // 追加到尾部 tail = e; // 更新尾指针

重要:尾插法使得多线程操作时,链表不会反转顺序,从根本上避免了 1.7 那种因顺序反转导致的环形链表。

改进点2:更细粒度的状态管理

java

oldTab[j] = null; // ⭐ 关键:立即清空旧桶

这减少了多个线程同时操作同一链表的窗口期。

3. 但 1.8 仍存在的并发问题

问题1:数据丢失(未解决)

java

// 两个线程同时执行 put,可能丢失数据 Thread1: 检查table[i]==null,准备插入 Thread2: 检查table[i]==null,准备插入 // 两者都认为自己是第一个,后插入的会覆盖先插入的
问题2:size 计算不准确

java

// size 的更新不是原子的 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // ... if (++size > threshold) // ⚠️ 多线程同时执行会出问题 resize(); // ... }
问题3:红黑树并发问题

java

// 链表转红黑树时,多个线程可能创建多个红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); // 多个线程可能重复树化

4. 并发问题复现代码(1.8)

java

public class HashMapConcurrentIssue { public static void main(String[] args) throws InterruptedException { Map<String, Integer> map = new HashMap<>(); // 两个线程同时put不同的key,但hash冲突 Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { map.put("key" + i, i); } }); Thread t2 = new Thread(() -> { for (int i = 10000; i < 20000; i++) { map.put("key" + i, i); } }); t1.start(); t2.start(); t1.join(); t2.join(); // 结果大概率不是20000,说明有数据丢失 System.out.println("Size: " + map.size()); // 可能输出 19987 等 } }

五、HashMap 1.7 vs 1.8 并发安全性对比

方面JDK 1.7JDK 1.8说明
死循环✅ 肯定发生⚠️ 几乎不可能1.8尾插法避免反转
数据丢失✅ 存在✅ 存在并发put仍会覆盖
size准确❌ 不准❌ 不准计数器非原子
红黑树安全-⚠️ 可能重复树化链表转树非原子
get阻塞✅ 会(死循环)⚠️ 可能读到中间状态虽无死循环,但数据可能不一致

六、HashMap 并发问题解决方案

方案1:使用 ConcurrentHashMap(首选)

java

// 完全线程安全的HashMap替代方案 Map<String, Integer> safeMap = new ConcurrentHashMap<>(); // ConcurrentHashMap 1.8 实现原理: // 1. 分段锁 → CAS + synchronized(1.8优化) // 2. 扩容时:多线程协助迁移 // 3. size:使用 CounterCell(类似LongAdder)

方案2:使用 Collections.synchronizedMap

java

// 包装类,所有方法加synchronized Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>()); // 优点:简单 // 缺点:性能差(全局锁)

方案3:使用读写锁

java

public class ReadWriteMap<K, V> { private final Map<K, V> map = new HashMap<>(); private final ReadWriteLock lock = new ReentrantReadWriteLock(); public V put(K key, V value) { lock.writeLock().lock(); try { return map.put(key, value); } finally { lock.writeLock().unlock(); } } public V get(K key) { lock.readLock().lock(); try { return map.get(key); } finally { lock.readLock().unlock(); } } }

七、为什么 HashMap 不设计为线程安全?

设计哲学:权衡的艺术

java

// HashMap 的设计选择 public class DesignRationale { /* 1. 性能优先:95%的使用场景是单线程 synchronized HashMap 会让单线程性能下降 10-20% 2. 分离关注点:并发需求用 ConcurrentHashMap // 单线程:HashMap(更快) Map<String, Object> singleThreadMap = new HashMap<>(); // 多线程:ConcurrentHashMap(安全) Map<String, Object> concurrentMap = new ConcurrentHashMap<>(); 3. 历史兼容:早期设计如此,改变会影响太多代码 */ }

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案

【点击此处即可/免费获取】​​​

实际工程建议

java

public class BestPractice { // ✅ 正确的使用方式 public void correctUsage() { // 场景1:方法内局部变量(线程安全) public void process() { Map<String, Object> localMap = new HashMap<>(); // ✅ // 仅当前线程使用 } // 场景2:只读的共享变量 private static final Map<String, String> CONSTANT_MAP; static { Map<String, String> temp = new HashMap<>(); temp.put("key", "value"); CONSTANT_MAP = Collections.unmodifiableMap(temp); // ✅ } // 场景3:需要线程安全的共享变量 private final Map<String, Object> sharedMap = new ConcurrentHashMap<>(); // ✅ } // ❌ 错误的使用方式 public void wrongUsage() { // 错误:多线程共享非线程安全集合 public static Map<String, Object> globalMap = new HashMap<>(); // ❌ // 错误:认为1.8的HashMap是线程安全的 // "1.8只是避免了死循环,并没有解决所有并发问题!" } }

八、总结

  1. JDK 1.7 死循环:确实存在,由头插法+链表反转在多线程下导致环形链表

  2. JDK 1.8 "解决"

    • 解决了死循环问题:改用尾插法,链表顺序不变

    • 未解决数据完整性问题:仍有数据丢失、size不准等问题

    • ⚠️未解决线程安全问题:HashMap 依然不是线程安全的// 单线程/线程封闭:HashMap(性能优)

  3. // 多线程共享:ConcurrentHashMap(线程安全) // 简单包装:Collections.synchronizedMap(性能差)

核心结论:无论 JDK 1.7 还是 1.8,HashMap 都不是线程安全的。1.8 只是修复了最危险的死循环 bug,但并发下的数据一致性问题依然存在。在多线程环境中,应始终使用ConcurrentHashMap

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/30 4:07:16

Mac端免费Gif录制神器:GifCapture完整使用手册

Mac端免费Gif录制神器&#xff1a;GifCapture完整使用手册 【免费下载链接】GifCapture &#x1f3c7; Gif capture app for macOS 项目地址: https://gitcode.com/gh_mirrors/gi/GifCapture 你是否曾经为了录制一个简单的屏幕操作而安装复杂的软件&#xff1f;或者因为…

作者头像 李华
网站建设 2026/5/28 10:49:41

终极时间序列增强实战指南:从问题诊断到智能调优

还在为时间序列数据样本不足、模型过拟合而苦恼吗&#xff1f;Time-Series-Library项目的数据增强功能正是你需要的解决方案。本文将带你从实际问题出发&#xff0c;通过智能增强策略快速提升预测性能&#xff0c;免费获取完整增强方案。 【免费下载链接】Time-Series-Library …

作者头像 李华
网站建设 2026/5/29 22:18:03

Simple Icons 开源品牌图标库的替代应用方案

Simple Icons 开源品牌图标库的替代应用方案 【免费下载链接】simple-icons 项目地址: https://gitcode.com/gh_mirrors/sim/simple-icons 在当今数字化时代&#xff0c;品牌标识的视觉呈现已成为项目开发中不可或缺的要素。然而&#xff0c;开发者们常常面临一个共同难…

作者头像 李华
网站建设 2026/5/29 11:16:35

【企业Agent安全管控必修课】:Docker权限管理的5大核心实践

第一章&#xff1a;企业Agent的Docker权限管理概述在现代企业级容器化部署中&#xff0c;Agent 通常以独立服务形式运行于 Docker 容器内&#xff0c;负责监控、日志收集或任务调度等关键职能。由于其需要与宿主机及容器运行时深度交互&#xff0c;如何合理分配 Docker 权限成为…

作者头像 李华
网站建设 2026/5/28 10:49:41

Untrunc视频修复大师:专业级损坏视频拯救方案

Untrunc视频修复大师&#xff1a;专业级损坏视频拯救方案 【免费下载链接】untrunc Restore a truncated mp4/mov. Improved version of ponchio/untrunc 项目地址: https://gitcode.com/gh_mirrors/un/untrunc 在数字时代&#xff0c;视频文件损坏已成为困扰无数用户的…

作者头像 李华
网站建设 2026/5/28 10:49:13

阅读APP书源配置完全指南:从零开始搭建个人图书馆

阅读APP书源配置完全指南&#xff1a;从零开始搭建个人图书馆 【免费下载链接】Yuedu &#x1f4da;「阅读」APP 精品书源&#xff08;网络小说&#xff09; 项目地址: https://gitcode.com/gh_mirrors/yu/Yuedu 想要在阅读APP中畅享海量网络小说资源&#xff1f;掌握书…

作者头像 李华