11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

组合涟漪
• 阅读 7790

jdk1.7 hashmap的循环依赖问题是面试经常被问到的问题,如何回答不好,可能会被扣分。今天我就带大家一下梳理一下,这个问题是如何产生的,以及如何解决这个问题。

一、hashmap的数据结构

先一起看看jdk1.7 hashmap的数据结构

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

数组 + 链表

hashmap会给每个元素的key生成一个hash值,然后根据这个hash值计算一个在数组中的位置i。i不同的元素放在数组的不同位置,i相同的元素放在链表上,最新的数据放在链表的头部。

往hashmap中保存元素会调用put方法,获取元素会调用get方法。接下来,我们重点看看put方法。

二、put方法

重点看看put方法

public V put(K key, V value) {
     if (table == EMPTY_TABLE) {
        inflateTable(threshold);
     }
     if (key == null)
         return putForNullKey(value);
     //根据key获取hash 
     int hash = hash(key);
     //计算在数组中的下表
     int i = indexFor(hash, table.length);
     //变量集合查询相同key的数据,如果已经存在则更新数据
     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;
}

再看看addEntry方法

void addEntry(int hash, K key, V value, int bucketIndex) {
     // 当数组的size >= 扩容阈值,触发扩容,size大小会在createEnty和removeEntry的时候改变
     if ((size >= threshold) && (null != table[bucketIndex])) {
         // 扩容到2倍大小,后边会跟进这个方法
         resize(2 * table.length);
         // 扩容后重新计算hash和index
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);
     }
     // 创建一个新的链表节点,点进去可以了解到是将新节点添加到了链表的头部
     createEntry(hash, key, value, bucketIndex);
 }

看看resize是如何扩容的

 void resize(int newCapacity) {
     Entry[] oldTable = table;
     int oldCapacity = oldTable.length;
     if (oldCapacity == MAXIMUM_CAPACITY) {
     threshold = Integer.MAX_VALUE;
     return;
     }
     // 创建2倍大小的新数组
     Entry[] newTable = new Entry[newCapacity];
     // 将旧数组的链表转移到新数组,就是这个方法导致的hashMap不安全,等下我们进去看一眼
     transfer(newTable, initHashSeedAsNeeded(newCapacity));
     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];
             //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next
             //这两行代码决定了倒序插入
             //比如:以前同一个位置上是:3,7,后面可能变成了:7、3
             newTable[i] = e;
             //将下一个元素赋值给当前元素,以便遍历下一个元素
             e = next;  
         }  
     }
}

我来给大家分析一下,为什么这几个代码是头插法,网上很多技术文章都没有说清楚。

三、头插法

我们把目光聚焦到这几行代码:

 //获取下一个元素,记录到一个临时变量,以便后面使用
 Entry<K,V> next = e.next;
 // 计算节点在新数组中的下标
 int i = indexFor(e.hash, newCapacity);
 // 将旧节点插入到新节点的头部
 e.next = newTable[i];
 //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next
 newTable[i] = e;
 //将下一个元素赋值给当前元素,以便遍历下一个元素
 e = next;

假设刚开始hashMap有这些数据

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

调用put方法需要进行一次扩容,刚开始会创建一个空的数组,大小是以前的2倍,如图所示:

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

开始第一轮循环:

 //next= 7   e = 3  e.next = 7
 Entry<K,V> next = e.next;
 // i=3
 int i = indexFor(e.hash, newCapacity);
 //e.next = null ,刚初始化时新数组的元素为null
 e.next = newTable[i];
 //给新数组i位置 赋值 3
 newTable[i] = e;
 // e = 7
 e = next;

执行完之后,第一轮循环之后数据变成这样的

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

再接着开始第二轮循环:

 //next= 5   e = 7  e.next = 5
 Entry<K,V> next = e.next;
 // i=3
 int i = indexFor(e.hash, newCapacity);
 //e.next = 3 ,此时相同位置上已经有key=3的值了,将该值赋值给当前元素的next
 e.next = newTable[i];
 //给新数组i位置 赋值 7
 newTable[i] = e;
 // e = 5
 e = next;

上面会构成一个新链表,连接的顺序正好反过来了。

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

由于第二次循环时,节点key=7的元素插到相同位置上已有元素key=3的前面,所以说是采用的头插法。

四、死循环的产生

接下来重点看看死循环是如何产生的?

假设数据跟元素数据一致,有两个线程:线程1 和 线程2,同时执行put方法,最后同时调用transfer方法。

线程1 先执行,到  Entry<K,V> next = e.next; 这一行,被挂起了。

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

此时线程1 创建的数组会创建一个空数组

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

接下来,线程2开始执行,由于线程2运气比较好,没有被中断过,执行完毕了。

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

过一会儿,线程1被恢复了,重新执行代码。

 //next= 7   e = 3  e.next = 7
 Entry<K,V> next = e.next;
 // i = 3
 int i = indexFor(e.hash, newCapacity);
 // e.next = null,刚初始化时新数组的元素为null
 e.next = newTable[i];
 // 给新数组i位置 赋值 3
 newTable[i] = e;
 // e = 7
 e = next;

这时候线程1的数组会变成这样的

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

再执行第二轮循环,此时的e=7

 //next= 3   e = 7  e.next = 3
 Entry<K,V> next = e.next;
 // i = 3
 int i = indexFor(e.hash, newCapacity);
 // e.next = 3,此时相同位置上已经有key=3的值了,将该值赋值给当前元素的next
 e.next = newTable[i];
 // 给新数组i位置 赋值 7
 newTable[i] = e;
 // e = 3
 e = next;

这里特别要说明的是 此时e=7,而e.next为什么是3呢?

因为hashMap的数据是公共的,还记得线程2中的生成的数据吗?

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

此时e=7,那么e.next肯定是3。

经过上面第二轮循环之后,线程1得到的数据如下:

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

此时由于循环判断还没有退出,判断条件是: while(null != e),所以要开始第三轮循环:

 //next= null   e = 3  e.next = null
 Entry<K,V> next = e.next;
 // i = 3
 int i = indexFor(e.hash, newCapacity);
 // e.next = 7,关键的一步,由于第二次循环是 key:7 .next = key:3,现在key:3.next = key:7
 e.next = newTable[i];
 // 给新数组i位置 赋值 3
 newTable[i] = e;
 // e = null
 e = next;

由于e=null,此时会退出循环,最终线程1的数据会是这种结构:

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

key:3 和 key:7又恢复了刚开始的顺序,但是他们的next会相互引用,构成环形引用。

注意,此时调用hashmap的get方法获取数据时,如果只是获取循环链上key:3 和 key:7的数据,是不会有问题的,因为可以找到。就怕获取循环链上没有的数据,比如:key:11,key:15等,会进入无限循环中导致CPU使用率飙升。

五、如何避免死循环

为了解决这个问题,jdk1.8把扩容是复制元素到新数组由 头插法 改成了 尾插法 。此外,引入了红黑树,提升遍历节点的效率。在这里我就不过多介绍了,如果有兴趣的朋友,可以关注我的公众号,后面会给大家详细分析jdk1.8的实现,以及 jdk1.7、jdk1.8 hashmap的区别。

此外,HashMap是非线程安全的,要避免在多线程的环境中使用HashMap,而应该改成使用ConcurrentHashMap。

所以总结一下要避免发生死循环的问题的方法:改成ConcurrentHashMap

如果这篇文档对您有所帮助的话,麻烦关注一下我的公众账号:苏三说技术,或者帮忙点赞或转发,坚持原创不易,您的支持是我坚持最大的动力。后面我会分享更多更实用的干货,谢谢大家的支持。

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
2年前
什么是跨域以及如何解决?通俗易懂带你彻底搞定
什么是跨域以及如何解决?通俗易懂带你彻底搞定现在的web项目,很多都是前后端分离,特别容易出现跨域问题那么什么是跨域问题呢?本篇文章带你彻底从本质上弄明白什么是跨域问题以及如何解决一跨域有什么现象?我们先看一下
Stella981 Stella981
3年前
Python Challenge Level 18
初学Python,挑战一下流行的PythonChallenge,很不幸,卡在了18关~~被字符字节码之间的转换搞得焦头烂额,不过终于搞定了还是很happy的~~~主要的问题就是16进制形式的字符如何转成字节码(注意:不是encoding)如:\'89','50','4e','47','0d','0a','1a','0a','00
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
MySQL数据库InnoDB存储引擎Log漫游(1)
作者:宋利兵来源:MySQL代码研究(mysqlcode)0、导读本文介绍了InnoDB引擎如何利用UndoLog和RedoLog来保证事务的原子性、持久性原理,以及InnoDB引擎实现UndoLog和RedoLog的基本思路。00–UndoLogUndoLog是为了实现事务的原子性,
可莉 可莉
3年前
11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的
!(https://oscimg.oschina.net/oscnet/d453e1c3841f4bc9a99ec584cf6f7d94.gif)点击上方蓝字关注我!jdk1.7hashmap的循环依赖问题是面试经常被问到的问题,如何回答不好,可能会被扣分。今天我就带大家一下梳理一下,这个问题是如何产生的
Easter79 Easter79
3年前
Spring如何解决循环依赖的问题
        在关于Spring的面试中,我们经常会被问到一个问题,就是Spring是如何解决循环依赖的问题的。这个问题算是关于Spring的一个高频面试题,因为如果不刻意研读,相信即使读过源码,面试者也不一定能够一下子思考出个中奥秘。本文主要针对这个问题,从源码的角度对其实现原理进行讲解。1\.过程演示        关于Spring
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这