news 2026/3/12 17:29:40

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/3/11 23:58:57

Mybatis入门简介HelloMybatis

Mybatis-9.28 环境&#xff1a; JDK1.8Mysql5.7Maven 3.6.1IDEA 回顾&#xff1a; JDBCMysqlJava 基础 &#xff08;封装继承的思想&#xff0c;看到一个东西能不能把它封装成一个工具类&#xff09;Maven &#xff08;会加个Maven、知道父子模块&#xff09;Junit &#x…

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

Java毕设选题推荐:基于SpringBoot的校园购物系统设计与实现基于springboot的校园零售管理系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/11 3:21:37

0x3f第11天 动态规划课后习题

1.爬楼梯1.最关键的一点就是得知道dfs&#xff08;i&#xff09;代表的什么代表一直到台阶i的时候有多少种走法2.这样就能得到dfs&#xff08;i&#xff09;dfs&#xff08;i-1&#xff09;dfs&#xff08;i-2&#xff09;3.dfs&#xff08;0&#xff09; 1因为dfs&#xff08;…

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

Spring Boot快速集成MiniMax、CosyVoice实现文本转语音

在一些需要高质量文本转语音&#xff08;TTS&#xff09;的场景中&#xff08;比如&#xff1a;有声书配音、播客等&#xff09;。之前介绍的EdgeTTS方案可能效果没有那么好。此时就比较推荐使用 MiniMax、CosyVoice这些提供的音色&#xff0c;这些音色的效果会更加拟人、逼真&…

作者头像 李华
网站建设 2026/3/6 16:59:54

逆向提示法:让大模型输出从平庸到专业的5步技巧

文章介绍"逆向提示"技巧&#xff0c;通过提供满意样例让模型反推提示词配方&#xff0c;解决AI内容同质化问题。该方法提炼语气、节奏、结构等要素&#xff0c;形成可复用模板&#xff0c;显著提升内容质量与一致性。作者提供社媒文案、产品描述等多场景应用案例&…

作者头像 李华
网站建设 2026/3/7 4:07:44

算法分析--基数排序

时间复杂度 O&#xff08;KN&#xff09;线性高位优先&#xff08;不好&#xff09;先按照高位升序排序&#xff0c;依次进行下去&#xff0c;直到排到最低位。image因为高位有一个分组的动作&#xff0c;在每个组里面对低位再排序。可以用递归。实际上&#xff0c;完全可以用低…

作者头像 李华