当前位置: 首页 > news >正文

头插法多线程不可用的原因

为什么头插法多线程下不可用?我们以HashMap扩容时用的头插法举例子:

JDK 1.7 HashMap 扩容时的头插法迁移逻辑

// 旧数组Entry[]oldTable=table;// 新数组(容量翻倍)Entry[]newTable=newEntry[oldCapacity*2];// 遍历旧数组的每个桶for(inti=0;i<oldTable.length;i++){// 拿到当前桶的头节点eEntry<K,V>e=oldTable[i];// 如果这个桶不为空,就遍历链表while(e!=null){// 【关键1】先保存e的下一个节点next// 因为接下来要修改e的next指针,不保存就会丢失后面的链表Entry<K,V>next=e.next;// 【关键2】计算e在新数组中的索引intnewIndex=indexFor(e.hash,newTable.length);// 【关键3】头插法:把e插入到新数组newIndex位置的链表头部// 步骤1:e的next指向新数组当前的头节点e.next=newTable[newIndex];// 步骤2:新数组的头指针指向enewTable[newIndex]=e;// 【关键4】处理下一个节点e=next;}}// 扩容完成,把table指向新数组table=newTable;

一、单线程下:头插法迁移的完整演示

我们用最简单的例子,一步一步看单线程下头插法是如何工作的。

初始状态

  • 旧数组容量 = 2
  • 桶 0 的链表:A → B → null
  • 假设 A 和 B 的 hash 值在新数组(容量 = 4)中计算出的索引都是 0(这样它们会被迁移到同一个桶)

执行迁移代码步骤

第一次循环(e = A)

  1. next = e.nextnext = B(保存 A 的下一个节点)
  2. 计算新索引:newIndex = 0
  3. 头插法插入 A 到新数组桶 0:
    • e.next = newTable[0]A.next = null(因为新数组桶 0 现在是空的)
    • newTable[0] = A→ 新数组桶 0:A → null
  4. e = nexte = B(准备处理下一个节点)

当前状态

  • 旧数组桶 0:A → B → null(还没修改)
  • 新数组桶 0:A → null

第二次循环(e = B)

  1. next = e.nextnext = null(保存 B 的下一个节点)
  2. 计算新索引:newIndex = 0
  3. 头插法插入 B 到新数组桶 0:
    • e.next = newTable[0]B.next = A(新数组桶 0 当前的头是 A)
    • newTable[0] = B→ 新数组桶 0:B → A → null
  4. e = nexte = null(循环结束)

最终状态

  • 旧数组桶 0:A → B → null
  • 新数组桶 0:B → A → null

关键点:原来的链表A → B,经过头插法迁移后,变成了B → A,顺序完全反转了。这是头插法最核心的特点,也是多线程并发下可能产生死循环的根源。

二、多线程下:死循环是如何一步步形成的?

现在我们有了头插法的基础,再回头看死循环,就会一目了然。

场景

还是用上面的例子:旧数组桶 0 的链表A → B → null,两个线程 T1 和 T2 同时扩容。

步骤 1:T1 执行到一半被挂起

T1 开始执行迁移代码,执行完第一次循环的第一步:

Entry<K,V>e=oldTable[i];// e = Awhile(e!=null){Entry<K,V>next=e.next;// next = B// 就在这里!T1的CPU时间片用完了,被挂起// 后面的代码还没执行intnewIndex=indexFor(e.hash,newTable.length);...}

此时 T1 的栈中保存着:

  • e = A
  • next = B

T1 还没有修改任何节点的 next 指针。


步骤 2:T2 完整完成了迁移

T2 获得 CPU 时间片,完整执行了整个迁移过程,和我们上面单线程的演示一样。

T2 执行完成后:

  • T2 的新数组桶 0:B → A → null
  • 全局的节点指针被修改了:
    • B.next = A
    • A.next = null

这是最关键的一步!节点是存在于堆内存中的,所有线程共享。T2 修改了 A 和 B 的 next 指针,T1 对此完全不知情。


步骤 3:T1 被唤醒,继续执行

T1 从挂起的地方恢复执行,它的栈中还是:

  • e = A
  • next = B

T1 会继续执行它剩下的代码:

T1 继续执行第一次循环(e = A)
// 已经执行过的:Entry<K,V> next = e.next; // next = BintnewIndex=indexFor(e.hash,newTable.length);// newIndex = 0// 头插法插入A到T1自己的新数组桶0e.next=newTable[newIndex];// A.next = null(T1的新数组桶0是空的)newTable[newIndex]=e;// T1的新数组桶0:A → nulle=next;// e = B(准备处理下一个节点)

当前 T1 的状态:

  • T1 的新数组桶 0:A → null
  • e = B
T1 执行第二次循环(e = B)
Entry<K,V>next=e.next;// 【致命一步!】// 现在e是B,B的next已经被T2改成了A!// 所以 next = A,而不是我们预期的null!intnewIndex=indexFor(e.hash,newTable.length);// newIndex = 0// 头插法插入B到T1的新数组桶0e.next=newTable[newIndex];// B.next = A(T1的新数组桶0当前头是A)newTable[newIndex]=e;// T1的新数组桶0:B → A → nulle=next;// e = A(准备处理下一个节点)

当前 T1 的状态:

  • T1 的新数组桶 0:B → A → null
  • e = A
T1 执行第三次循环(e = A)
Entry<K,V>next=e.next;// A.next = null(这个是对的)intnewIndex=indexFor(e.hash,newTable.length);// newIndex = 0// 头插法插入A到T1的新数组桶0e.next=newTable[newIndex];// A.next = B(T1的新数组桶0当前头是B)newTable[newIndex]=e;// T1的新数组桶0:A → B → A → ...e=next;// e = null(循环结束)

最终结果:T1 的新数组桶 0 中,链表变成了:A → B → A → B → ...一个无限循环的闭环!


三、为什么 JDK 1.7 要用头插法?

既然头插法有这么大的问题,为什么 JDK 1.7 还要用它?

  1. 性能原因:头插法不需要遍历链表找尾部,插入速度是 O(1),比尾插法快很多。
  2. 设计初衷:HashMap 本来就不是为多线程环境设计的,设计者认为并发场景应该用Hashtable或者ConcurrentHashMap

四、JDK 1.8 的解决方案

JDK 1.8 将扩容时的插入方式改为尾插法,迁移时会保持链表的原有顺序,不会出现反转,因此彻底避免了循环引用的产生。

再次强调:即使 JDK 1.8 解决了死循环问题,HashMap 仍然不是线程安全的。多线程环境下仍然会出现数据丢失、覆盖等问题,并发场景请务必使用ConcurrentHashMap

http://www.jsqmd.com/news/706238/

相关文章:

  • 现代CSS实战:玻璃拟态风格健康科普网站的设计与实现
  • 机器学习算法选择指南:构建高效算法清单
  • 1.5小时下载1.5万次:Bitwarden CLI供应链攻击敲响密码安全警钟
  • 别再只用K-Means了!用MATLAB手把手教你搞定更抗噪的K-Medoids聚类(附完整代码)
  • 深度学习训练指标可视化:工具与实践指南
  • 2026年第二季度马鞍形屋面板排行:混凝土马鞍板/钢筋混凝土双t板/预应力双t板/马鞍板屋面/马鞍板屋顶/双t坡板/选择指南 - 优质品牌商家
  • Fastboot Enhance:快速掌握Android设备管理的终极图形化解决方案
  • 为什么92%的AI PoC项目因容器隔离失效被叫停?Docker Sandbox 6步硬核配置手册(含GPU透传避坑指南)
  • 终极分屏游戏指南:NucleusCoop让单机游戏变多人同屏神器
  • FloPy 完整指南:Python 驱动的 MODFLOW 地下水建模终极解决方案
  • 如何用Logitech鼠标宏实现PUBG零后坐力压枪?3步快速上手指南
  • 如何在5分钟内掌握GoldHEN作弊管理器:PS4游戏修改终极指南
  • 深度学习中梯度爆炸问题与梯度裁剪技术详解
  • LSTM时间序列预测中的权重正则化实践与优化
  • 极域电子教室控制解除指南:3步解锁你的学习自由
  • 可复用Agent开发框架、多智能体协同系统、安全管控方案
  • Keras深度学习多分类任务实战与优化技巧
  • 如何快速搭建个人哔咔漫画离线图书馆:picacomic-downloader完整指南
  • 终极解密指南:如何永久解锁科学文库和国家标准的加密文档
  • 专栏B-产品心理学深度-04-稀缺性策略
  • 【VS Code Dev Containers 面试通关宝典】:20年资深架构师亲授12个高频真题+避坑口诀
  • 计算机视觉工具:Python+OpenCV的常用函数汇总
  • Ruby JSON
  • Bebas Neue:开源几何无衬线字体在现代化设计中的技术架构与应用实践
  • 从零搭建AI开发环境:手把手教你用Anaconda管理多个PyTorch+CUDA版本(Ubuntu 20.04/22.04实测)
  • Zotero SciPDF插件:终极免费文献PDF自动下载完整指南
  • 2026可靠电动单梁起重机标杆名录:轨道式集装箱门式起重机、轻小型起重机、通用桥式起重机、防爆桥式起重机、冶金桥式起重机选择指南 - 优质品牌商家
  • Keras序列填充与截断技术详解
  • AD8232心电监测系统:如何用开源硬件突破生物电信号采集的技术壁垒?
  • 从电池装配到整车下线:YC8000-Q赋能三菱PLC的产线互联方案