【HashMap篇】JDK1.7多线程下resize()导致最终产生循环链表的原因

HashMap死循环问题大讲解(图解) 图解HasmMap死循环的产生及其解决方案 阅读详情

问题

由于HashMap并非是线程安全的,所以在高并发的情况下必然会出现问题。

如果是在单线程下使用HashMap,自然是没有问题的,如果后期由于代码优化,这段逻辑引入了多线程并发执行,在一个未知的时间点,会发现CPU占用100%,居高不下,通过查看堆栈,你会惊讶的发现,线程都Hang在hashMap的get()方法上,服务重启之后,问题消失,过段时间可能又复现了。

这是为什么?

原因分析

在了解来龙去脉之前,我们先看看HashMap的数据结构。

在内部,HashMap使用一个Entry数组保存key、value数据,当一对key、value被加入时,会通过一个hash算法得到数组的下标index,算法很简单,根据key的hash值,对数组的大小取模 hash & (length-1),并把结果插入数组该位置,如果该位置上已经有元素了,就说明存在hash冲突,这样会在index位置生成链表。

如果存在hash冲突,最惨的情况,就是所有元素都定位到同一个位置,形成一个长长的链表,这样get一个值时,最坏情况需要遍历所有节点,性能变成了O(n),所以元素的hash值算法和HashMap的初始化大小很重要。

当插入一个新的节点时,如果不存在相同的key,则会判断当前内部元素是否已经达到阈值(默认是数组大小的0.75),如果已经达到阈值,会对数组进行扩容,也会对链表中的元素进行rehash。

实现
HashMap的put方法实现:

1、判断key是否已经存在

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 如果key已经存在,则替换value,并返回旧值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    // key不存在,则插入新的元素
    addEntry(hash, key, value, i);
    return null;
}

2、检查容量是否达到阈值threshold

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

如果元素个数已经达到阈值,则扩容,并把原来的元素移动过去。

3、扩容实现

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ...

    Entry[] newTable = new Entry[newCapacity];
    ...
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

这里会新建一个更大的数组,并通过transfer方法,移动元素。

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

移动的逻辑也很清晰,遍历原来table中每个位置的链表,并对每个元素进行重新hash,在新的newTable找到归宿,并插入。

案例分析

假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第3个节点就会扩容,为了验证效果,假设负载因子是1.

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

以上是节点移动的相关逻辑。
在这里插入图片描述

插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组。
在这里插入图片描述

假设 线程2 在执行到Entry<K,V> next = e.next;之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。

线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点

第一步,移动节点a
在这里插入图片描述

第二步,移动节点b
在这里插入图片描述

注意,这里的顺序是反过来的,继续移动节点c
在这里插入图片描述

这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:
在这里插入图片描述

这时,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。

Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;

执行之后的引用关系如下图
在这里插入图片描述

执行后,变量e指向节点b,因为e不是null,则继续执行循环体,执行后的引用关系
在这里插入图片描述

变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下:
1、执行完Entry<K,V> next = e.next;,目前节点a没有next,所以变量next指向null;
2、e.next = newTable[i]; 其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
3、newTable[i] = e 把节点a放到了数组i位置;
4、e = next; 把变量e赋值为null,因为第一步中变量next就是指向null;
所以最终的引用关系是这样的:
在这里插入图片描述

节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。

另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。

总结

HashMap死循环只发生在JDK1.7版本中,主要原因是在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。

JDK1.8版本做了什么解决了这个HashMap死循环问题

在JDK1.8中,HashMap改成了尾插法,解决了链表死循环的问题

避免HashMap发生死循环常用的解决方案:

1)是用安全线程ConcurrentHashMap代替HashMap(推荐使用)

2)使用线程安全容器HashTable代替,但是性能低(不推荐使用)

3)使用Synchronized或Lock加锁后,再进行操作,相当于多线程排队执行,也会影响性能(不建议使用)

HashMap(1.7)在多线程并发扩容时导致的链表死循环问题 图文说明1.7的HashMap在多线程并发扩容时导致的链表死循环问题 阅读详情

相关推荐

HashMap在JDK1.7中多线程并发会出现死循环,超详细图解

就是JDK1.7的头插法造成了循环问题,因此在JDK1.8中对HashMap的扩容方法进行了改进,从头插法改成了尾插法。

qq_57031340的博客 1843

深入理解Java集合之HashMap死循环

一、问题 由于HashMap并非是线程安全的,所以在高并发的情况下必然会出现问题, 如果是在单线程下使用HashMap,自然是没有问题的,如果后期由于代码优化,这段逻辑引入了多线程并发执行,在一个未知的时间点,会发现CPU占用100%,居高不下,通过查看堆栈,你会惊讶的发现,线程都Hang在hashMap的get()方法上,服务重启之后,问题消失,过段时间可能又复现了。 二、原因分析 HashMa...

qq_42499816的博客 791

大多数人不知道的:HashMap链表成环的原因和解决方案

前言: 在 JDK7 版本下,很多人都知道 HashMap 会有链表成环的问题,但大多数人只知道,是多线程引起的,至于具体细节的原因,和 JDK8 中如何解决这个问题,很少有人说的清楚,百度也几乎看不懂,本文就和大家聊清楚两个问题: JDK7 中 HashMap 成环原因; JDK8 中是如何解决的。 一、JDK7 中 HashMap 成环原因 成环的时机 HashMap 扩容时。 多线程环境下。 成环的具体代码位置 在扩容的 transfer 方法里面,有三行关键的代码,如下: void tra

m0_45270667的博客 6722

HashMap中是如何形成环形链表的?

HashMap 是非线程安全的。多线程环境下,HashMap的put操作会引起死循环,导致CPU的利用率接近100%,因为多线程会导致HashMap的Entry链表形成环形链表,导致Entry的next节点永远不为空,就会产生死循环获取Entry。 那么这个环形链表是如何形成的? 转载原文如下: 导读:经过前面的博客总结,可以知道的是,HashMap是有一个一维数组和一个链表组成,从而得知,在解决冲突问题时,hashmap选择的是链地址法。为什么HashMap会用一个数组这链表组成,当时给出的答案

独行侠的守望の博客 4520

多线程环境下HashMap之死循环

HashMap是非线程安全的,多线程下一般使用ConcurrentHashMap代替,那为什么HashMap在多线程下不安全呢?ConcurrentHashMap又是如何改进的?JDK1.8中又如何改进的?

水不可追 710

QT下利用FFTW实现功率谱密度分析(周期图法)

写在前面 出于项目需求,需要在QT的msvc 2015编译环境下实现功率谱密度估计。 所以这个需求很直接的关键词就是 1. C++ 2. QT 3. 傅里叶变换 4. 功率谱密度估计 但是在网上搜索了很久,没有一个非常直接的答案。所以我觉得很有必要写一篇文章来说明,在QT msvc2015 32bit环境下如何实现功率谱密度估计。 本文并没有经过严格编辑,但代码是已经可以跑通的,如...

李时珍食疗志的博客 5223

jdk1.7 HashMap中的致命错误:循环链表

jdk1.7 HashMap中的"致命错误":循环链表 jdk1.7 HashMap结构图 jdk1.7是数组+链表的结构 jdk1.7版本中主要存在两个问题 头插法会造成循环链表的情况 链表过长,会导致查询效率下降 jdk1.8版本针对jdk1.8进行优化 使用尾插法,消除出现循环链表的情况 链表过长后,转化为红黑树,提高查询效率 具体可以参考我的另一篇博客你真的懂大厂面试题:...

zycxnanwang的博客 6013

【亲测有效】OpenClaw Gateway 出现 “(no output)“ 卡死问题完整解决指南:从手动重启到 systemd Watchdog 自动恢复

最近在使用 OpenClaw(自托管 AI 助手框架,前身 Clawdbot/Moltbot)时,遇到了一个非常常见的顽疾:TUI(终端界面)或 Telegram 等通道经常显示 "(no output)",消息发出去后机器人完全没反应,看起来像是卡死了。

hampeter的博客 8609

JDK1.7中HashMap采用头插法扩容时的产生的链表循环问题

在JDK1.7时,HashMap的结构主要为数据+链表,在扩容时采用头插法,在单线程时表现正常,但在多线程时存在着链表循环的问题,下面对该问题来进行一个分析。 HashMap的容量是有限的,当元素插入一个接近饱和的HashMap之时,则会触发HashMap的扩容机制,也就是Resize。 触发Resize有两个因素 Capacity LoadFactor 即当HashMap.Size >= Capacity * LoadFactor时,便进行Resize操作 Resize的具体操作 扩容 创建

茶颜悦色 2114

从零开始:TexLive(Latex)安装与配置全指南

本文提供了一份详尽的TexLive(Latex)安装与配置全指南,旨在帮助用户从零开始快速搭建LaTeX工作环境。内容涵盖从镜像下载、安装步骤详解、环境验证到高效编辑器(如VS Code)配置,并特别强调了中文排版支持与宏包管理,确保用户能顺利处理学术论文等专业文档的排版需求。

weixin_29284885的博客 903

osg3.6.5---osgEarth3.2 实现数字地球的加载

目前我在网上找得比较多的是osgearth2.10及以下的版本及加载数字地球的方法,但是我使用的是osg3.6.5和osgEarth3.。但是3.x版本和2.x版本差别很大,主要区别在于2.x有7个主要库,而3.x版本只有osgEarth这一个库。所以,特别记录一下。后面我讲他集成到qt的窗体在上传更新新的代码。1.直接上源码pro文件和cpp文件。

枫影竹韵的博客 1008

jdk1.7 HashMap扩容时死循环问题

jdk1.7 hashmap在resize时进行扩容时,会导致死循环,主要是因为jdk1.7采用的是头插法 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next;

xby7437的博客 1万+

华为OD机试真题2025双机位B卷 Java&Go 实现【朋友圈个数】

给定一组朋友关系,统计一下该朋友关系网中的朋友圈个数。 朋友圈的定义:一个朋友圈至少由3个朋友组成,且要求同一个朋友圈中的任意两个人都具有直接的朋友关系。 输入描述 输入一个朋友关系列表,如 Fiends =A.B],[A.C],IB,DI,其中的每一个元素 Friendsi表示 Friends[i][0)和 Friends [i][1] 是朋友关系 先输入一个数字 N 代表关系的总数,后面每条关系一行,两个成员以逗号分隔

MISAYAONE的博客 518

面试必问的HashMap源码put()方法和resize()方法——基于JDK1.8

目录一、前言二、 HashMap 的构造方法三、 put()方法(一)、源码注释(二)、流程图四、 resize()方法(一)、源码注释(二)、流程图(三)、 resize方法的注意事项 一、前言 HashMap太太太常用了,不做过多的介绍。进入正文直接冲源码。 本文主要是基于1.8的HashMap讲述部分源码,主要重点是put()方法和resize()方法 二、 HashMap 的构造方法 /** * 设置初始化容量,调用的是下面的重载方法 */ public HashMap(int initialCap

爱读史的小马的博客 2570

42、多媒体与虚拟现实在艺术和影视领域的创新应用

本文探讨了多媒体与虚拟现实在艺术和影视领域的创新应用,涵盖三个典型案例:影视与人类学融合项目通过协作模式模糊作者与观众界限,推动文化文献的发展;交互式数字短片《分手》利用基于对象的广播技术探索个性化影视体验;威尼斯双年展瑞士馆4D项目则通过虚拟现实技术实现文化遗产的数字化展示。这些案例展示了技术在叙事、用户体验及文化保护中的深度应用,并展望了未来技术融合、个性化深化与文化遗产传承的发展趋势。

kappa的博客 114

优快云专栏限时活动!福利公告必读!

本人优快云付费专栏周排名27名专业博主,每周文章平均阅读量约7w以上!

FJN110的博客 3153

探究JDK7 HashMap并发扩容导致CPU100%原因

HashMap之所以在并发下的扩容造成死循环,是因为,**多个线程并发进行时,因为一个线程先期完成了扩容,将原的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序链表。于是形成了一个环形链表。**当调用get操作并命中链环的桶时,就会进入一个死循环的情况,将CPU的消耗到100%。 ...

fangmeng1997的博客 1151

LIS2DH三轴加速度传感器原理图设计,已量产(加速度传感器)

最近在做一款可穿戴设备原型时,选了 LIS2DH 这款加速度计 —— 比起之前常用的 ADXL345,它在低功耗和稳定性上的优化刚好戳中了可穿戴设备 “续航 + 可靠” 的核心需求。下面结合这次画的原理图,聊聊这个小模块的硬件设计细节。

不脱发的程序猿 389

JDK7中HashMap链表成环的原因和解决方案

jdk1.7 HashMap扩容转移链表时形成环状链表的原因 JDK8 中解决方案(从这里查看的) JDK 8 中扩容时,已经没有 JDK7 中的 transfer 方法了,而是自己重新写了扩容方法,叫做 resize,链表从老数组拷贝到新数组时的代码如下: //规避了8版本以下的成环问题 else { // preserve order // loHead 表示老值,老值的意思是扩容后,该链表中计算出索

Hanyinh的博客 1433

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

HashMap扩容机制(jdk1.7与jdk1.8的不同)

判断容量上限:若已达最大容量,修改阈值并直接返回,不扩容;计算新容量 / 新阈值:容量翻倍,阈值同步翻倍;创建新数组:根据新容量初始化哈希桶数组;遍历旧数组:逐个处理每个桶位的节点;分场景迁移节点单节点:直接计算新索引赋值;红黑树:执行树节点拆分与迁移;链表:拆分为高低位链表,批量迁移,保持顺序;替换引用:将table指向新数组,旧数组等待垃圾回收。扩容核心目标:动态调整数组容量,降低哈希冲突概率,维持 HashMap 高性能;核心规则:容量翻倍增长,数据迁移采用高低位链表拆分。

2502_92082922的博客 1084

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

HashMap为什么不是线程安全?

前言 该试题从互联网获得,真实性没有考究,加上本人学识浅薄,所以面试题参考为主,解析分享为主。 若对解析有不同看法,还请评论指正。谢谢。 HashMap为什么不是线程安全? 以JDK1.8的HashMap为例,引用作者: 一字马胡 所写文章中的一张图: 上图为 ...

大雄君的博客 2万+

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Hashmap底层实现原理(JDK1.7)

在JDK1,7中,HashMap 数据结构为 数组+链表.三句话,说清它的数据结构:1.整体是一个数组; 2.数组每个位置是一个链表; 3.链表每个节点中的Value即我们存储的Object; 初始化HashMap: 提供了有参构造和无参构造: 1.无参构造中,容器默认的数组大小 initialCapacity 为 16; 2.,有参构造中,指定数组长度时,如果指定数组的长度为9,但是真正在初始化时创建数组的长度不是为9而是16。 创建数组是在使用put方法时进行初始化创建。 进入inflateTable(

m0_49797779的博客 964

HashMap源码分析(JDK1.7)

基于JDK1.7的HashMap源码分析 本文将会从以下几个方面对JDK7源码进行解析,记录自己学习过程。 一:JDK7HashMap介绍 二:构造器 三:put方法 四:扩容 五:get方法 六:常见面试题 一、JDK7HashMap介绍 JDK1.7中HashMap底层是数组+链表 DEFAULT_INITIAL_CAPACITY 默认初始化容量,必须是2的次幂 ...

ji1162765575的博客 219

new hashmap 初始大小_面试官:说一下HashMap原理,循环链表是如何产生的

Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 JDK1.7 和 1.8 中具体实现稍有不同。今天我们只讲解JDK1.7版本的HashMap。1、HashMap的数据结构图是一个数组+链表结构2、HashMap成员变量/** * The default initial capacity...

weixin_39783857的博客 149

HashMap头插法多线程形成环,导致死循环

过程 在 jdk 1.7 及以前HashMap使用头插法,多线程的情况下容易形成环。 假设在下标为1处有一个长度为2的链表,如果再次插入一个节点,就需要resize()扩容。 此时两个线程A和B执行put()操作,A线程先执行,执行完transfer () 中的Entry<K,V> next = e.next;被挂起,此时e指针指向5,next指向7,如图所示。 此时线程B执行,将数组扩大两倍,链表仍散列在下标为1处,由于使用了头插法,节点位置将会交换,如图所示。 这时候线程A又执行,

See_Csdn_的博客 2129

python面试 hashmap是什么_Java面试必问之Hashmap底层实现原理(JDK1.8)

1. 前言上一篇从源码方面了解了JDK1.7中Hashmap的实现原理,可以看到其源码相对还是比较简单的。本篇笔者和大家一起学习下JDK1.8下Hashmap的实现。JDK1.8中对Hashmap做了以下改动。默认初始化容量=0引入红黑树,优化数据结构将链表头插法改为尾插法,解决1.7中多线程循环链表的bug优化hash算法resize计算索引位置的算法改进先插入后扩容2. Hashmap中put...

weixin_39852953的博客 124
上一篇: 【RabbitMQ篇】消息堆积/丢失 解决办法
下一篇: Kafka 为什么这么快 ?
_Romeo
_Romeo 优质创作者: 后端开发技术领域 优质创作者: 后端开发技术领域
博客等级 码龄8年 2万+粉丝 104原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Romeo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值