HashMap死循环
我记得第一次听到HashMap会有死循环的问题是两年前,我们团队有一个学长实习回来给我们说有个同事因为没有注意到HashMap在多线程可能会造成死循环而造成了一些损失。后面就对这个问题很感兴趣,然后趁着今天整理资料就顺便记录一下,虽然偷懒了,逃 = -=
什么造成的
我们知道HashMap不是线程安全的,那么具体到代码,是哪部分代码的原因呢?
我本来想写一篇博客介绍如何死循环产生的原因及过程,然后再查资料的时候发现了美团点评的这篇博客:Java 8系列之重新认识HashMap中介绍死循环的部分已经写的非常清楚了,就不想做重复工作了 = -=,哈哈
但是呢,最主要的问题解决了,我就只好分析一下JDK1.8中是如何做到扩容后链表依然保持着原来的顺序喽~哈哈
resize()方法
JDK1.8的改变
相较于JDK1.7,在1.8中resize()方法不再调用transfer()方法,而是直接将原来transfer()方法中的代码写在自己方法体内;
当然表面上我们能看到方法的减少,其实还有一个重大改变,那就是:扩容后,新数组中的链表顺序依然与旧数组中的链表顺序保持一致!
数组长度的技巧
在分析源码之前,我们还需要知道HashMap底层数组的一些优化:
数组长度总是2的倍数,扩容则是直接在原有数组长度基础上乘以2。
为什么要这么做?有两个优点:
1. 通过与元素的hash值进行与操作,能够快速定位到数组下标
相对于取模运算,直接进行与操作能提高计算效率。在CPU中,所有的加减乘除都是通过加法实现的,而与操作时CPU直接支持的。
2. 扩容时简化计算数组下标的计算量
因为数组每次扩容都是原来的两倍,所以每一个元素在新数组中的位置要么是原来的index,要么index = index + oldCap,假设
数组长度:2,key:3
0 0 0 1
0 0 1 1 结果为 0 0 0 1 = 1 所以数组下标为1;
扩容后,数组长度:4,key:3
0 0 1 1
0 0 1 1 结果为 0 0 1 1 = 3 = 原来的index + oldCap = 1 + 2
即确定元素在新数组的下标时,我们只需要检测元素的hash值与oldCap作与操作的结果是否为0:为0,那么下标还是原来的下标;为1,那么下标等于原来下标加上旧数组长度。
我们来看关键代码(resize()方法完整代码附后):
//如果扩容后,元素的index依然与原来一样,那么使用这个head和tail指针
Node<K,V> loHead = null, loTail = null
//如果扩容后,元素的index=index+oldCap,那么使用这个head