展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > JDK1.7HashMap多线程扩容为什么会死循环示例详解

JDK1.7HashMap多线程扩容为什么会死循环示例详解

JDK1.7HashMap多线程扩容时,头插法迁移节点会修改共享节点的next指针,多个线程交叉操作可能导致环形链表,查询时陷入死循环。JDK1.8改用尾插法避免了该问题,但HashMap仍非线程安全,多线程环境应使用ConcurrentHashMap。

一、前言

说到 Ja va 里的 HashMap,几乎每个开发者都打过交道。作为最常用的数据结构之一,它的底层实现其实并不复杂——早期版本的 HashMap 主要依赖数组加链表来组织数据:

JDK1.7HashMap多线程扩容为什么会死循环示例详解

数组 + 链表

到了 JDK 1.8,又多了一个红黑树结构:

数组 + 链表 + 红黑树

但提起老版本(JDK 1.7)的 HashMap,就绕不开一个经典的坑——多线程环境下同时扩容,可能导致链表形成环,进而引发死循环。

这个问题的根源其实很清晰:

JDK 1.7 HashMap 扩容时使用头插法;
头插法会修改节点的 next 指针;
多个线程同时扩容时,会操作同一批节点对象;
最终可能导致 A.next = B,B.next = A,形成环。

看上去有点抽象?别急,我们一步步拆开来看。

二、HashMap 扩容是不是在原数组上改?

当然不是。Ja va 数组一旦创建,长度就固定了,没法原地“变大”。比如你声明一个长度为 16 的数组,不可能让它直接变成 32。

所以 HashMap 扩容的流程是这样的:

1. 创建一个更大的新数组
2. 遍历旧数组中的节点
3. 把旧节点重新挂到新数组中
4. 最后让 table 指向新数组

换句话说,就是:

oldTable 长度 16
扩容后创建 newTable 长度 32
最后 table = newTable

但这里有一个关键点——数组是新的,但节点对象不是新的。

什么意思呢?扩容时并不会复制节点本身,而是把旧数组里的节点对象“搬”到新数组里。例如旧数组中有:

oldTable[3] -> 节点1 -> 节点2 -> null

扩容后,并不是在 newTable 里创建新的节点1和节点2,而是直接复用原来的节点对象:

newTable[3] -> 原来的节点1 / 原来的节点2

节点对象是复用的,这一点非常重要。

三、为什么放到新数组里还要修改 next?

因为数组每个位置只能放一个头节点。如果多个元素命中了同一个桶,就必须通过链表连接起来。比如:

newTable[5] -> 节点1 -> 节点2 -> null

真正维护链表关系的是 next 指针:

节点1.next = 节点2
节点2.next = null

所以扩容迁移时,必然要重新整理节点之间的 next 指针——这也是问题滋生的土壤。

四、JDK 1.7 的头插法是什么?

JDK 1.7 的扩容迁移用的是头插法。核心逻辑简化一下就是:

Entry next = e.next;                // 先保存旧链表中的下一个节点
int i = indexFor(e.hash, newCapacity);   // 计算新数组下标
e.next = newTable[i];                    // 当前节点指向新桶原来的头节点
newTable[i] = e;                         // 当前节点成为新桶的新头节点
e = next;                                // 继续处理下一个旧节点

最核心的一步就是 e.next = newTable[i]——这句话会修改当前节点的 next 指针。

五、单线程下头插法为什么没问题?

假设旧链表是:

节点1 -> 节点2 -> null

扩容时,一开始新数组桶为空:newTable[i] = null

先迁移节点1:

节点1.next = newTable[i];   // 即 null
newTable[i] = 节点1;

新链表变成:newTable[i] -> 节点1 -> null

再迁移节点2:

节点2.next = newTable[i];   // 此时 newTable[i] 是节点1
newTable[i] = 节点2;

新链表变成:newTable[i] -> 节点2 -> 节点1 -> null

看到了吗?原来的链表顺序被反转了:

原链表:节点1 -> 节点2 -> null
新链表:节点2 -> 节点1 -> null

单线程下这没问题——只是顺序反了,但链表最终仍然指向 null,一切正常。

六、多线程扩容为什么会出问题?

问题就出在:两个线程同时扩容同一个 HashMap。

假设旧数组某个桶中挂了两个节点:

oldTable[3] -> 节点1 -> 节点2 -> null

现在两个线程同时触发了扩容:线程A创建了 newTableA,线程B创建了 newTableB,这两个新数组彼此独立。但注意——

节点1和节点2是同一批旧节点对象。

也就是说,两个线程操作的是同一个节点1和同一个节点2。这就埋下了冲突的种子。

七、详细模拟多线程扩容过程

第一步:线程A开始扩容

线程A准备迁移节点1。它先执行了:

e = 节点1;
next = e.next;

此时:e = 节点1,next = 节点2——线程A已经记住了节点1后面是节点2。

但就在这个时候,线程A被 CPU 暂停了。当前旧链表还是完好的:

节点1 -> 节点2 -> null

第二步:线程B开始并完成扩容

线程B也开始处理同一条旧链表。注意,此时节点1和节点2的 next 还是原始状态。

线程B先迁移节点1。它自己的新桶为空:newTableB[i] = null。执行头插法:

节点1.next = newTableB[i];   // 即 null
newTableB[i] = 节点1;

新链表:newTableB[i] -> 节点1 -> null

然后迁移节点2。此时 newTableB[i] = 节点1:

节点2.next = newTableB[i];   // 即 节点1
newTableB[i] = 节点2;

新链表:newTableB[i] -> 节点2 -> 节点1 -> null

此时,真实世界中节点对象的关系已经变成:

节点2.next = 节点1
节点1.next = null

也就是:节点2 -> 节点1 -> null

注意——线程B修改的是节点对象自己的 next 字段,而不是只修改它自己的数组。

八、线程A恢复执行,问题出现

线程A之前暂停时保存了 e = 节点1,next = 节点2。现在它恢复执行,继续迁移节点1。

线程A自己的新桶为空:newTableA[i] = null。执行头插法:

节点1.next = newTableA[i];   // 即 null
newTableA[i] = 节点1;

线程A的新链表变成:newTableA[i] -> 节点1 -> null

然后线程A执行 e = next,因为之前保存的 next 是节点2,所以 e = 节点2。

九、线程A处理节点2

线程A处理节点2时,先取 next = 节点2.next。

但是!节点2的 next 已经被线程B改过了(线程B之前执行过 节点2.next = 节点1)。所以线程A拿到的 next = 节点1。

然后线程A把节点2头插到自己的新数组中:

节点2.next = newTableA[i];   // 此时 newTableA[i] = 节点1
newTableA[i] = 节点2;

等价于 节点2.next = 节点1。线程A的新链表变成:newTableA[i] -> 节点2 -> 节点1 -> null

接着,线程A执行 e = next,而 next = 节点1——所以线程A又回到了节点1。

十、线程A再次处理节点1,形成环

此时线程A的新桶头节点是节点2:newTableA[i] = 节点2。线程A再次处理节点1,执行头插法:

节点1.next = newTableA[i];   // 即 节点2
newTableA[i] = 节点1;

等价于 节点1.next = 节点2。

但前面已经有 节点2.next = 节点1。于是链表变成了:

节点1 -> 节点2 -> 节点1 -> 节点2 -> ...

环形链表,形成了。

十一、为什么形成环后会死循环?

HashMap 查询元素时,会沿着链表一直往后找。核心逻辑类似:

while (e != null) {
    if (e.key.equals(key)) {
        return e.value;
    }
    e = e.next;
}

正常链表最终会走到 null,比如 节点1 -> 节点2 -> null。

但如果链表形成了环,比如 节点1 -> 节点2 -> 节点1 -> 节点2 -> ...,那么 e 永远不会变成 null。程序就会陷入无限循环,CPU 占用飙升,看起来就是“卡死了”。

这就是 JDK 1.7 HashMap 多线程扩容死循环问题的完整链条。

十二、关键问题:为什么各自扩容还会互相影响?

核心原因在于:数组是各线程自己的,但节点对象是共享的。

线程A有自己的 newTableA,线程B有自己的 newTableB,但里面存放的地址指向同一批旧节点对象——并不是复制节点。

所以,虽然数组不同,但两个线程修改的是同一个节点对象里的 next 字段。可以把节点理解成这样一个类:

class Entry {
    K key;
    V value;
    Entry next;
}

数组只是保存节点地址:Entry[] table;

扩容时那句 e.next = newTable[i],直接修改了节点对象内部的 next。所以即使没有动旧数组 oldTable[i],也会改变旧节点之间的链表关系。

十三、JDK 1.8 是怎么改进的?

JDK 1.8 对 HashMap 做了几个重要优化,一劳永逸地解决了这个问题。

1. 扩容时不再使用头插法

JDK 1.8 扩容时,会把原桶中的链表拆成两条:

lo 链表:留在原位置
hi 链表:移动到 原位置 + oldCap

判断条件很简单:

if ((e.hash & oldCap) == 0) {
    // 留在原位置
} else {
    // 移动到 原位置 + oldCap
}

2. 使用尾插法,保持链表顺序

JDK 1.7 的头插法会反转链表,而 JDK 1.8 改用尾插法,尽量保持原来的顺序。这就避免了头插法带来的典型成环问题。

3. 引入红黑树

当某个桶里的链表过长,并且数组长度达到一定条件时,链表会转成红黑树,避免查询效率下降。

所以结构变成了:

JDK 1.7:数组 + 链表
JDK 1.8:数组 + 链表 + 红黑树

十四、但是 JDK 1.8 的 HashMap 线程安全吗?

不安全。

虽然 JDK 1.8 优化了扩容逻辑,避免了头插法导致的死循环,但 HashMap 本身仍然不是线程安全的。在多线程环境下,多个线程同时读写仍可能出现:

数据覆盖
数据丢失
size 不准确
结构异常

所以,多线程环境下别用普通的 HashMap,老老实实用 ConcurrentHashMap。

十五、面试总结版

如果面试官问起这个问题,可以这样回答:

JDK 1.7 的 HashMap 在扩容时会创建新数组,并把旧数组中的节点迁移过去。数组是新的,但节点对象是复用的,迁移时会修改节点的 next 指针。JDK 1.7 使用头插法迁移链表,这会把链表顺序反转。单线程下没问题,但多线程同时扩容时,多个线程会操作同一批节点对象。如果线程A暂停,线程B完成扩容并反转了链表,线程A恢复后继续使用之前保存的节点引用,就可能出现 节点1.next = 节点2,节点2.next = 节点1,形成环。之后执行 get() 操作时,HashMap 会沿着链表一直查找,永远走不到 null,导致死循环,CPU 飙高。

JDK 1.8 改用尾插法和高低位链表拆分,避免了头插法导致的成环问题。但 HashMap 仍然不是线程安全的,多线程环境下应使用 ConcurrentHashMap。

十六、一句话总结

JDK 1.7 HashMap 多线程扩容死循环的本质是:新数组是各线程自己的,但节点对象是共享的;头插法迁移会修改节点的 next 指针,多个线程交叉修改后可能形成环形链表,导致查询时永远走不到 null。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
精品专题 更多
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

IOS软件

正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

AI

正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

Mac软件 更多
WPS Office macOS版
WPS Office macOS版

WPS Office是一款面向macOS用户的综合办公套件,将文字处理、电子表格、演示文稿、PDF工具、模板与云协作整合在同一应用中。它兼容常见Microsoft Office格式,支持Intel及Apple芯片Mac,基础功能可免费使用,

Shapr3D macOS版
Shapr3D macOS版

Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。

REAPER macOS版
REAPER macOS版

REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。

Ableton Live macOS版
Ableton Live macOS版

Ableton Live 是面向音乐制作人与现场表演者的数字音频工作站,提供编曲视图、独具特色的现场视图、音频录制、MIDI创作、实时变速、乐器及效果器。Mac版原生支持Apple芯片,并可连接音频接口、MIDI控制器和第三方插件。

Adobe After Effects macOS版
Adobe After Effects macOS版

Adobe After Effects 是面向动态图形、影视特效与视频合成的专业创作软件。它提供关键帧动画、遮罩抠像、运动跟踪、三维空间、文字动画和表达式等工具,并可与 Premiere Pro、Photoshop、Illustrator

Adobe Audition macOS版
Adobe Audition macOS版

Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

Adobe Bridge macOS版
Adobe Bridge macOS版

Adobe Bridge 是面向摄影师、设计师及内容团队的免费数字资产管理工具,可集中预览照片、视频和 Adobe 项目文件,并利用评级、标签、关键词、元数据、筛选与收藏集完成分类检索。它还支持批量重命名、格式导出、相机导入及 Camera

Adobe Illustrator macOS版
Adobe Illustrator macOS版

Adobe Illustrator 是 Adobe 推出的专业矢量图形设计软件,适合在 Mac 上制作标志、图标、插画、包装、信息图及印刷版面。它以路径和锚点构建可无损缩放的图形,并提供钢笔、形状、文字、图像描摹、渐变、画板及生成式功能。

Adobe InDesign macOS版
Adobe InDesign macOS版

Adobe InDesign 是面向印刷品与数字出版物的专业版面设计软件,可在 Mac 上制作书籍、杂志、宣传册、海报、报告及交互式文档。它提供精细的文字样式、网格、母版、长文档管理和印前输出能力。

Adobe Lightroom Classic macOS版
Adobe Lightroom Classic macOS版

Adobe Lightroom Classic 是面向桌面摄影工作流的照片管理与后期处理软件,可将原始照片保存在本地硬盘,通过目录、关键词、评分和收藏夹高效整理图库,并提供 RAW 冲印、蒙版、镜头校正、批量同步及多格式导出等功能。

Adobe Media Encoder macOS版
Adobe Media Encoder macOS版

Adobe Media Encoder 是 Adobe 推出的专业音视频编码与转码工具,可通过队列、预设和监视文件夹批量完成格式转换、代理文件创建及多平台成片输出,并与 Premiere Pro、After Effects 等软件紧密协作。

Adobe Photoshop macOS版
Adobe Photoshop macOS版

Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理软件,提供图层、蒙版、选区、修复、调色、文字排版、智能对象及生成式编辑等能力。Mac 版支持 Intel 与 Apple Silicon 处理器。

WINDOWS 更多
Adobe Photoshop Windows版
Adobe Photoshop Windows版

Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许

Adobe InDesign Windows版
Adobe InDesign Windows版

Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator

Adobe Illustrator Windows版
Adobe Illustrator Windows版

Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。

Adobe Bridge Windows版
Adobe Bridge Windows版

Adobe Bridge 是 Adobe 推出的创意资产管理工具,可集中预览、分类和查找照片、视频、PDF及多种设计文件,并支持元数据、关键词、评级、批量重命名和格式导出。它可与 Photoshop、Illustrator 等应用衔接,

Affinity Photo Windows版
Affinity Photo Windows版

Affinity Photo 是一款面向摄影师、修图师和视觉设计人员的桌面图像编辑工具,提供 RAW 显影、多图层合成、蒙版、实时滤镜、批处理和色彩管理等功能。原独立产品已停止销售,其照片编辑能力已延续至免费的新 Affinity 应用;

Adobe Lightroom Classic Windows版
Adobe Lightroom Classic Windows版

Adobe Lightroom Classic 是面向 Windows 桌面摄影工作流的照片管理与无损编辑工具,可通过目录集中整理大型图库,并完成 RAW 显影、局部蒙版、批量调整、打印和多格式导出。它强调本地文件管理与精细控制,

Figma Windows版
Figma Windows版

Figma 是面向产品设计与开发团队的云端协作平台,可在 Windows 桌面客户端中完成界面绘制、组件管理、交互原型、评论评审和开发交付。文件保存在云端,多名成员能够同步编辑并共享设计系统;官方同时提供免费 Starter 方案及按席位计

Affinity Designer Windows版
Affinity Designer Windows版

Affinity Designer 是面向矢量图形、数字插画和界面设计的专业创作工具,可在矢量、像素与导出工作区之间快速切换。它提供曲线编辑、形状构建、画板、排版、图层和专业色彩管理,并支持常见设计文件的导入及多种印刷、网页格式输出。

Capture One Pro Windows版
Capture One Pro Windows版

Capture One Pro 是面向摄影师和影像团队的专业桌面照片处理工具,提供相机定制色彩配置、RAW转换、图层与蒙版、联机拍摄、目录及会话管理等功能。它适合棚拍、人像、商业产品、婚礼和风光摄影,

Adobe Acrobat Pro Windows版
Adobe Acrobat Pro Windows版

Adobe Acrobat Pro 是面向专业 PDF 工作的综合解决方案,可在 Windows 中创建、编辑、转换、比较、整理和保护文档,并支持表单、电子签名、扫描件文字识别及敏感信息永久遮盖。订阅还包含网页、移动服务和云端协作能力,

Adobe After Effects Windows版
Adobe After Effects Windows版

Adobe After Effects 是面向动态图形、动画与影视视觉特效制作的专业工具,可完成关键帧动画、图层合成、遮罩跟踪、抠像、文字动画和二维三维元素整合。Windows 版通过 Adobe Creative Cloud 安装及更新,

DaVinci Resolve Windows版
DaVinci Resolve Windows版

DaVinci Resolve是Blackmagic Design推出的一体化影视后期工具,将素材管理、非线性剪辑、专业调色、Fusion视觉特效、Fairlight音频处理和成片交付整合在统一工程中。免费版已覆盖多数个人创作需求,