news 2026/5/15 9:04:28

腾讯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/1 16:28:54

条码标签管理工具BarTender在制造业中的典型应用场景解析

在制造业数字化转型过程中&#xff0c;数据的“最后一公里”往往发生在现场。设备、物料、半成品、成品如何被准确识别、快速流转、全程追溯&#xff0c;是很多企业在推进 MES、QMS、WMS 时绕不开的问题。 在这一过程中&#xff0c;智能标签与标识系统成为连接“物理世界”与“…

作者头像 李华
网站建设 2026/5/1 17:55:53

OpenAPI 到底是什么?从规范原理到落地实战的通关指南

简单来说&#xff0c;OpenAPI 就是描述 HTTP API 的“世界通用语”。 在没有 OpenAPI 之前&#xff0c;后端写 Word 文档&#xff0c;前端靠猜&#xff0c;测试靠问。而 OpenAPI 的出现&#xff0c;彻底统一了这套流程。 把时间拨回 2015 年&#xff0c;SmartBear 将大名鼎鼎的…

作者头像 李华
网站建设 2026/5/9 6:04:13

嵌入式分析型数据库的5个实战技巧:从零到高性能应用

嵌入式分析型数据库的5个实战技巧&#xff1a;从零到高性能应用 【免费下载链接】duckdb 项目地址: https://gitcode.com/gh_mirrors/duc/duckdb 还在为数据处理性能瓶颈而烦恼&#xff1f;是否曾因传统数据库的复杂部署和维护成本而犹豫不决&#xff1f;嵌入式分析型数…

作者头像 李华
网站建设 2026/5/3 18:53:12

37、深入理解 TCP/IP 网络编程

深入理解 TCP/IP 网络编程 1. IP 主机与 IP 地址 主机是支持 TCP/IP 协议的计算机或设备,每台主机由一个 32 位的 IP 地址来标识。为了方便,32 位 IP 地址通常用点分十进制表示,例如 134.121.64.1。主机也有主机名,如 dns1.eecs.wsu.edu。在实际应用中,应用程序通常使用…

作者头像 李华
网站建设 2026/5/14 14:24:26

42、MySQL编程:C与PHP的实现

MySQL编程:C与PHP的实现 1. C语言中的MySQL编程 在C语言中进行MySQL编程,主要涉及数据库表的创建、数据插入以及查询结果的获取等操作。 1.1 数据库表操作 以下是一段示例代码,展示了如何在C语言中删除已存在的 students 表,创建新的 students 表,并插入学生记录:…

作者头像 李华
网站建设 2026/5/14 13:36:39

【React性能优化实战指南:从入门到精通-web技术栈】

作为前端开发者&#xff0c;你是否遇到过React应用卡顿、渲染缓慢的问题&#xff1f;本文将深入剖析React性能优化的核心技巧和常见痛点&#xff0c;帮助你打造丝滑流畅的用户体验。 一、React性能问题的常见痛点 1.1 不必要的重渲染 这是React应用中最常见的性能杀手。每次父…

作者头像 李华