news 2026/4/18 23:29:16

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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。

写入时机

即时写入

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

延迟写入

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

并发死循环

存在

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

已解决

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


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

0x3f第十天复习(考研日2)(9.18-12.30,14.00-15.00)

二叉搜索树验证 前序2min ac4min ac4min ac1min ac二叉搜索树验证 中序 6min x 基本没问题&#xff0c;记得 每次递归都要return 结果 6min ac 4min ac3min ac二叉搜索树验证 后序 30min x 最后return min(lmin,x), max(rmax,x) 还是有点没理解 15min ac 10min x还是不理解 (r…

作者头像 李华
网站建设 2026/4/18 10:49:29

医疗AI智能体架构设计:六大核心模块与七种专业智能体类型全解析

文章介绍了医疗AI智能体的六大核心模块框架&#xff1a;感知、对话接口、交互系统、工具集成、记忆学习和推理&#xff0c;以及七种专业智能体类型的特点与应用场景。这一模块化架构旨在构建安全、可解释且自适应的医疗AI系统&#xff0c;推动人工智能在医疗领域的深度应用&…

作者头像 李华
网站建设 2026/4/17 6:38:33

js函数声明和函数表达式的理解

在JS中,函数声明会被提升&#xff0c;这意味着函数可以在声明之前被调用。当你使用函数声明的方式定义函数: function resizeFn() {...}整个函数声明会被提升到作用域的顶部。这意味着在整个作用域内&#xff0c;无论函数在何处声明&#xff0c;都可以在声明前调用。函数声明会…

作者头像 李华
网站建设 2026/4/18 3:27:40

奶奶都能看懂的 C++ —— vector 与迭代器

但是在讲解它之前&#xff0c;我们需要先了解迭代的对象是什么。常见的一种&#xff0c;叫做 vector。vector 类型使用可变有序序列我们知道&#xff0c;数学里&#xff0c;vector 是向量的意思。但 C 里的向量和它不太一样。它的含义是&#xff0c;具有可变元素个数的有序对象…

作者头像 李华
网站建设 2026/4/18 3:37:33

基于java的SpringBoot/SSM+Vue+uniapp的校园活动管理系统的详细设计和实现(源码+lw+部署文档+讲解等)

文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言 &#x1f31e;博主介绍&#xff1a;✌全网粉丝15W,CSDN特邀作者、211毕业、高级全…

作者头像 李华