HashMap
在并发执行put操作时会引起死循环
,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
产生死循环的过程
(仅在jdk1.7有效, 1.8由于变成了尾插法, 虽然也有并发线程安全问题, 但是不会造成死循环了)
/**对HashMap进行容量扩充
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {//遍历原table中的所有表头
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {//依次将链表中的元素,重新添加到新的table中
Entry<K,V> next = e.next;// 代码 1
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
首先,假设我们有两个并发的线程同时进行容量扩充,因此每个线程都拥有一个newTable。状态如下所示:
再假设,线程1和线程2都执行了代码1。状态如下所示:
接着,线程1执行完整个链表转移操作,线程2什么都不做。状态如下所示:
注:这里假定元素E 1和E 2重新映射后在newTable中的索引位置不变(或者说一致),由于是头插法,故元素逆序。
接着,线程2向newTable 2中插入一个元素。状态如下所示:
注意:特别注意e 2和next 2的指针位置。
因为e 2!=null,因此线程2继续转移元素。状态如下所示:
因为e 2!=null,因此线程2继续转移元素。状态如下所示:
发现没有,此状态与上上步的状态一样,因此接下就进入了死循环。
一句话总结就是:
两个线程同时并发扩容, 其中一个线程因为时间片用完, 只执行了第一步指针指向节点, 将e和next指针指向了原来的hashmap节点, 但是第二个完成了扩容的全过程, 因为1.7是头插法, 导致了节点颠倒
, 所以导致了第一个线程的指针颠倒
, 并且出现了一个next指针指向e节点的一个指针, 这个是始料未及的, 也是出现死循环的关键.