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

jdk1.8 是如何解决死循环问题的?


首先先看看 hashmap 在 jdk1.8 下扩容的核心方法

在 JDK 1.8 的HashMap源码中,已经找不到transfer这个方法了。

JDK 1.8 将扩容逻辑全部整合到了resize()方法中。而且,为了配合新的“尾插法”和“位运算”优化,这段代码的逻辑发生了翻天覆地的变化。

如下是 JDK 1.8resize()方法中核心的数据迁移部分代码(去掉了红黑树转换等干扰逻辑,只看链表迁移):

// JDK 1.8 HashMap.resize() 的核心迁移逻辑片段 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 1. 创建新数组 // 遍历旧数组 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 如果这个位置有数据 oldTab[j] = null; // 把旧数组这个位置清空 if (e.next == null) // Case 1: 只有一个节点,直接算新位置放进去 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // Case 2: 红黑树的处理 (略) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // Case 3: 链表迁移 (核心重点!!!) // 定义了两组头尾指针,这就是“本地打包”的证据 Node<K,V> loHead = null, loTail = null; // 低位链表 (留在原位) Node<K,V> hiHead = null, hiTail = null; // 高位链表 (去 j + oldCap) Node<K,V> next; do { next = e.next; // 【核心改变1】:不需要重新取模 (hash % newCap) // 而是看最高位是 0 还是 1 if ((e.hash & oldCap) == 0) { // 最高位是 0,放到“低位链表” if (loTail == null) loHead = e; else loTail.next = e; // 【核心改变2】:尾插法!挂在当前尾巴后面 loTail = e; // 更新尾巴指针 } else { // 最高位是 1,放到“高位链表” if (hiTail == null) hiHead = e; else hiTail.next = e; // 【核心改变2】:尾插法! hiTail = e; } } while ((e = next) != null); // 循环处理链表 // 【核心改变3】:一次性写入新数组 if (loTail != null) { loTail.next = null; // 把尾巴封死,防止非法连接 newTab[j] = loHead; // 放入新数组原索引位置 } if (hiTail != null) { hiTail.next = null; // 把尾巴封死 newTab[j + oldCap] = hiHead; // 放入新数组 (原索引 + oldCap) 位置 } } } }

那么 jdk1.8 是如何解决死循环问题的呢?

根据上面的源码,解决方案其实非常简单粗暴,就是**“维持秩序”**。

在 1.7 中,死循环的根源在于:

线程 T2 把链表从A -> B改成了B -> A。 线程 T1 还在按A -> B的顺序处理,结果发现A的下一个变成了BB的下一个又变回了A,于是转圈圈。

在 1.8 中,使用了尾插法

  1. loTail.next = e;这一行代码保证了新加入的节点永远在屁股后面。
  2. T2 扩容完,链表是A -> B
  3. T1 醒来继续扩容,它顺着链表走,看到的顺序依然是A -> B
  4. 因为顺序没乱,B 永远不会指向 A
  5. 最后loTail.next = null这一行显式地把链表封口,彻底杜绝了环的产生。

还有一点变化, 除了变“尾插法”之外,“何时写入新数组”这个动作的时机也发生了根本性的变化。

1. JDK 1.7 的做法:搬一个扔一个 (即时写入)

在 JDK 1.7 的transfer方法中,处理链表是**“边拆边扔”**:

  • 动作:从旧箱子里拿出一个物品(节点),算一下新位置,立刻把它扔进新箱子(新数组newTable)里。

代码逻辑

// 伪代码 while(遍历旧链表) { Entry next = e.next; int i = indexFor(e.hash, newCapacity); // 每一个节点处理完,立刻修改新数组的内存 e.next = newTable[i]; // 头插 newTable[i] = e; // 写入新数组(这里是并发隐患点) e = next; }
  • 风险:每次循环都在修改共享内存(新数组),并发环境下这相当于在“裸奔”,极其容易产生竞争。

2. JDK 1.8 的做法:打包好了一次性搬 (延迟写入)

在 JDK 1.8 的resize方法中,处理链表是**“先分类打包,最后一次性扔过去”**:

  • 动作
  1. 先把旧箱子里的物品全部拿出来过一遍。根据 hash 值,在本地把它们分成两堆(两个临时链表):
  • 一堆是留在原索引位置的(loHead/loTail)。
  • 一堆是去新索引位置的(hiHead/hiTail)。
  • 循环结束了,再一次性把这两堆链表的头节点,挂到新数组的对应位置。

代码逻辑

// 伪代码 Node loHead = null, loTail = null; // 本地低位链表 Node hiHead = null, hiTail = null; // 本地高位链表 // 1. 先在本地循环构建链表 (不碰新数组) do { if (原位置) { loTail.next = e; // 尾插到本地链表 loTail = e; } else { hiTail.next = e; // 尾插到本地链表 hiTail = e; } } while (e = e.next); // 2. 循环结束,才去动新数组 (只写两次内存) if (loTail != null) newTab[j] = loHead; // 一次性挂上去 if (hiTail != null) newTab[j + oldCap] = hiHead; // 一次性挂上去

总结这种变化的意义

这个“本地链表”机制(实际上是使用了loHead/loTail等临时指针变量),配合尾插法,带来了两个巨大的好处:

  1. 减少了对共享内存(新数组)的写入次数
  • 1.7 是有多少个节点就写多少次新数组。
  • 1.8 是无论链表多长,一个桶只写 1-2 次新数组。
  1. 避免了中间状态的暴露
  • 1.7 搬运过程中,新数组里处于“半成品”状态(顺序颠倒、还在插),其他线程更容易读到脏数据或通过引用关系形成环。
  • 1.8 在本地拼好之前,新数组那个位置是空的或者旧的,直到最后拼好了才“原子性”地挂上去(虽然不是真正的原子操作,但大大缩短了不一致的时间窗口)。

还有一个优化:(e.hash & oldCap)

你可能注意到了源码里这一行:if ((e.hash & oldCap) == 0)。 这也是 1.8 的一大亮点,它利用了二进制的特性,避免了昂贵的取模运算。

  • 假设oldCap是 16 (10000),newCap是 32 (100000)。
  • 扩容其实就是把二进制的高位多看一位。
  • 如果 hash 值的那个多出来的位是0,元素就在原位 (j)
  • 如果 hash 值的那个多出来的位是1,元素就在原位 + 老容量 (j + oldCap)

这就是为什么源码里会有loHead(Low,原位) 和hiHead(High,高位) 这两个链表的原因。这让扩容效率极大提升。

总结:

JDK 1.8 通过尾插法保证了链表顺序,物理上消灭了环形链表产生的土壤;通过本地指针(lo/hi list)减少了对共享内存的写入频次。虽然HashMap在 1.8 依然是线程不安全的(多线程put可能覆盖数据),但至少不会把服务器 CPU 跑满了。

JDK 1.7 vs JDK 1.8 的核心区别总结

特性

JDK 1.7 (transfer 方法)

JDK 1.8 (resize 方法)

插入方式

头插法 (Head Insertion)

新节点插在链表头部。

尾插法 (Tail Insertion)

新节点插在链表尾部。

链表顺序

倒置

(A->B 扩容后可能变成 B->A)。

保持原序

(A->B 扩容后依然是 A->B)。

计算位置

重新 Hash

需要执行indexFor(h, newCapacity)

位运算判断

直接看hash & oldCap是 0 还是 1。

写入时机

即时写入

遍历一个节点,就往新数组里塞一个。

延迟写入

先在本地拼好链表,最后一次性挂到新数组。

并发死循环

存在

链表倒置 + 竞争 = 环形链表。

已解决

链表顺序不变,不会形成环。


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

相关文章:

  • 10个高效降AI率工具,MBA必备避坑指南
  • CPU RAM(内存) 是什么?一篇文章搞定入门!
  • Docker容器操作总结
  • 二叉树的“家谱学”:为什么最近公共祖先是最优解?
  • 【LEA-BP】基于爱情进化算法LEA优化BP神经网络的风电功率预测研究附Matlab代码
  • 【App Service】部署War包到Azure云上遇404错误
  • 重庆三峡学院图书资料管理系统设计与实现(源码+论文+部署+安装)
  • 神经网络和深度学习 第四周:深度神经网络的关键概念
  • 华为OD机考双机位C卷 - 计算误码率 (Java Python JS C/C++ GO )
  • 2025 国内整合营销服务商TOP10 评测!全链路赋能 + 标杆案例,十大品牌权威榜单发布,驱动品牌增长新引擎 - 全局中转站
  • openFeign - yebinghuai-qq
  • kubernetes终端管理神器
  • GPIO输入输出的内容补充(继上一篇)
  • 0x3f第十天复习(考研日2)(9.18-12.30,14.00-15.00)
  • 逆向提示法:让大模型输出从平庸到专业的5步技巧
  • DHCP服务器:轻松管理IP自动分配 - 详解
  • MySQL的这6大雷区,大部分人都会踩中!
  • redis-基本操作指令 - yebinghuai-qq
  • 医疗AI智能体架构设计:六大核心模块与七种专业智能体类型全解析
  • Java毕设项目:基于springboot的校园零售管理系统的设计与实现(源码+文档,讲解、调试运行,定制等)
  • vivado hls如何实现recursive fuction递归函数
  • Docker容器操作总结 - 十里
  • CF95D Horse Races
  • 程序员必备技能:AI Agent 9种设计模式深度解析,提升大模型应用效能(值得收藏)
  • 扩展域并查集(种类并查集)
  • 算法分析--基数排序
  • 【题解】P14826 踩踩标
  • 2025-12-21
  • 港媒盛赞“香港媳妇”徐冬冬!婚照惊艳全网,港圈作品圈粉无数
  • 2025 国内公关公司 TOP10 评测!策略创新+资源整合,十大品牌权威榜单发布,专业赋能品牌传播新生态 - 全局中转站