java7的HashMap造成死循环的通俗化讲解
在JDK1.7及以前的版本,如果在并发环境中使用HashMap保存数据,有可能会产生死循环的问题。产生这个问题是因为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];
//多线程情况下,上面创建好新的数组,死循环就是在下面方法中产生的
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
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;
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;
}
}
}
其中最重要的就是进行transfer方法的部分,此部分详细说明了头插法的步骤和变量赋值变化:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历原HashMap数组,e是数组元素,若有链表衔接,就表示链表中的元素
for (Entry<K,V> e : table) {
while(null != e) {
//这里就是将表示将e现在指代的元素的下一个元素赋给next这个变量
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];
//将e指代的元素放入新表中根据hash(key)计算过的位置
newTable[i] = e;
//e这时就会变成 Entry<K,V> next = e.next; 这里赋值的next的变量,也就是将上面的next 赋值给e
e = next;
}
}
}
单线程下是不会造成死循环的,只有在多线程下由于头插法的方式,就会造成死循环。
死循环过程:
-
1.假设原数组只有的大小只有2,这时有key(3),key(7),key(5)按照头插法插入了index:1形成链表,顺序如图,此时e指向a,next指向b,即:
e=key(3); next=key(7);
-
2.线程1执行完扩容后在 Entry<K,V> next = e.next;被挂起,紧接着线程2完成执行扩容的存放,到达如上状态,由于key(3),key(7)的rehash相同,所以他两个又被放入同一个index:3
-
3.此时线程1重新回来,继续执行,这时 e=key(3);
next=key(7); 执行完循环中的头插法语句newTable[i] = e; e这时是key(3)后:线程1的index:3中的第一个头插入元素就是key(3)- key3、5、7在内存中只有一份实例在堆中,方法栈中只是存有这些实例的引用,而且两个线程的rehash(key)是一样的,所以这时e所指向的key(3)和next指向的key(7)是和线程2中的指向是一样的
-
4.接着执行e = next;next此时是key(7),则:e=key(7);
e=key(7);
next=key(7);
- 5.进入第二次循环,执行**Entry<K,V> next = e.next;**这时:按照reahash后的链表顺序(引用顺序)key(7)–>key(3)
e=key(7);
next=key(3);
- 6.然后顺序执行接下来的**newTable[i] = e;**进行头插法,在线程2中将e=key(7)头插入key(3)之前,形成链表
- 7.接着执行e = next;next此时是key(3),则:e=key(3);
e=key(3);
next=key(3);
- 8.接着进入第三次循环,这一次就会造成循环指向,进入死循环。执行**Entry<K,V> next = e.next;**这时的e=key(3),按照链表顺序,e.next=null也即是next=null
- 9.接着进行头插法,**newTable[i] = e;**进行头插法,在线程2中将e=key(3)头插入key(7)之前,同时key(7)又会指向key(3)这样也就造成了循环指向,进入死循环
为甚么java8可以避免这个情况:
- 造成死循环的原因就是在扩容的时候采用头插法,改变了原本链表的顺序
- java8中采用的是尾插法,不会改变原本的顺序,也就会形成死循环
- 当然,如果要进行多线程编程,还是推荐使用ConcurrentHashMap