当前位置: 首页 > 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.cnnetsun.cn/news/174869.html

相关文章:

  • 【毕业设计】基于springboot的校园零售管理系统的设计与实现(源码+文档+远程调试,全bao定制等)
  • 硬件自查自纠!十年前的电脑可能还可以再战十年
  • 一键配置 Web 前端开发环境(PowerShell 自动化脚本)
  • 程序员必备技能:AI Agent 9种设计模式深度解析,提升大模型应用效能(值得收藏)
  • 【python大数据毕设实战】哮喘患者症状数据可视化分析系统、Hadoop、计算机毕业设计、包括数据爬取、数据分析、数据可视化、机器学习
  • 9 个降AI率工具,MBA 必备避坑指南
  • Windows系统文件inetmib1.dll丢失损坏 下载修复方法
  • Boost电路的右半平面零点
  • 【全球AI伦理治理】
  • 毕业季必看!7款免费AI写论文神器实测,一站式搞定选题、大纲到降重
  • LLMs之Survey之Agent:《Measuring Agents in Production》翻译与解读
  • 零代码上手Google Gemini 3:5种实用方法大揭秘
  • “你用的那个AI,到底把你坑了还是救了?”——解锁宏智树论文的协作新范式
  • 好写作AI:别等学校采购了!你的论文“救命神器”自己就能用上
  • Windows系统文件GdiPlus.dll丢失或损坏 下载修复方法
  • 研究生必备8款AI写论文神器:5分钟生成25000字问卷类论文,自动生成高信度数据
  • 【BuildFlow 筑流】unitrix_macros库 Cargo.toml 配置详解及依赖库用法
  • 《开发者出海必看:如何优雅地搞定海外服务支付?(保姆级干货)》
  • Thinkphp和Laravel企业防爆安全设备信息系统
  • Thinkphp和Laravel全家桶鲜花售卖商城系统vue
  • 记录我适配iOS26遇到的一些问题
  • 通过命令模拟pod创建
  • 同步机无感 STM32 低成本 MD500E 永磁同步控制方案大揭秘
  • 小宝玩具 【通达信、源码 、主图、附图】
  • 使用 Github Pages 和 Hexo
  • 审稿 一区期刊注意事项: journal offers the option to connec;please note, reviewers are not expected 是什么意思
  • 线性代数:多维世界的变形工具箱
  • 力扣题目142. 环形链表 II​的解法分享,附图解
  • MATLAB电力系统继电保护之自动重合闸
  • 10 个AI写作工具,助你轻松搞定继续教育论文!