在JDK1.7及以前的版本,如果在并发环境中使用HashMap保存数据,有可能会产生死循环的问题,造成cpu的使用率飙升。产生这个问题是因为JDK1.7及以前的版本中,HashMap扩容采用的是头插入,1.8做的改进是采用尾插法,所以不会造成死循环的问题。
首先,来看1.7扩容的代码:
//进行扩容时方法
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity]; //---------111
//多线程情况下,上面创建好新的数组,死循环就是在下面方法中产生的
transfer(newTable, initHashSeedAsNeeded(newCapacity)); //---------222
table = newTable; //---------333
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 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; //----------444
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;
}
}
}
一开始的数据结构如图:
假设两个线程,第一个线程执行到注释111处,cpu时间执行完了,轮到第二个线程执行,第二个线程执行完注释111处,进入到注释222的方法,执行到444处(这个地方很重要),这时变量e指向节点a,变量next指向节点b,第二个线程的cpu执行完了,这时候两个线程都创建了新的哈希表,如图;
又轮到第一个线程执行,假设a,b,c三个节点刚好映射到7这个位置,
先移动a节点,如图;
再移动节点b,如图;
最后移动节点c,如图;结合三张图片可以看出来,头插法,就是从链表的头部插入
这个时候线程1的时间片用完,也就是注释222的方法已经执行完毕,但是还没执行注释333,也就是内部的table还没有设置成新的newTable,这时候线程2开始执行,这时内部的引用关系如下:
再贴一下tranfer方法的代码,我们刚才说线程2执行到注释444处,这时变量e指向节点a,变量next指向节点b
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; //----------444
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;
}
}
继续执行,7会指向节点a;然后b成了变量e,
因为e不是null,则继续执行循环体,7会指向节点b,b指向a,因为上一张可以看出节点本来就指向a,所以变化如图,此时a就变成了节点e。
下面就是链表成环的关键;
e.next = newTable[i]; newTable[i]也就是7的位置,指向的是节点b,所以会把节点a的next指向b,而从上面的图可以看到,节点b的next是指向节点a的,这样就构成了死循环。
newTable[i] = e;将7的位置指向的是节点a。此时结构如图:
另外,可以看出,如果线程2执行到了注释333时,把newTable设置成到内部的table,节点c的数据就会丢了。
之前自己也没懂为什么1.7hashmap为造成死循环,看了下面这篇文章才看懂,推荐一下。
https://www.jianshu.com/p/1e9cf0ac07f4