前言
都知道HashMap是线程不安全的,现在说说他是怎么样的一个不安全法。
先给出结论:
HashMap的扩容操作在多线程场景下会出现问题——数组上的链表会成环。
下面来大致看看它是如何形成环的。
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];
newTable[i] = e;
e = next;
}
}
}
在这里我们假定以下特殊场景:
1.有两个线程共同使用一个HashMap,并且在put元素时同时达到了扩容条件,开始进行元素的转移。
2.这个HashMap的容量假设为4,扩容后为8,并且假设某一条链上的元素扩容后的索引还都是一样的(即还在一条链上)
3.两个线程先后执行到了第五行
4.第一个线程先执行第五行下面的代码。
明确以下事情:
扩容时各个线程都会在自己线程内部新建一个数组,完成扩容后将这个数组赋值给table
各个线程都会有自己的指针去标记原table的元素。
扩容之前如下图:
当两个线程执行到第五行的时候,如下图:
两个线程分别有(e1,next1)和(e2,next2)两套各自的线程内变量指向线程共享的table数组。之后线程开始将table的元素转移到自己线程内新建的数组中,第一个线程先完成了这个操作,如下:
注意第二个线程中的e2,next2这时候的位置(指向entry1和entry2)由于线程1将entry1entry2进行了转移,所以这俩指针位置现在看起来是也跟着过来了。
这时候线程2开始转移,线程而并不知道entry1entry2已经换了位置了,所以线程2相当于把线程一中的数组的元素已到了自己线程中的数组中,移动完后如下图:
可以看到在线程2中的数组中出现了环。随后线程2会将这个带环的数组赋值给HashMap 的table变量。在之后的get或put的时候(需要遍历链表的时候)就会出现死循环。