我们都是知道HashMap是线程不安全的,那么它在多线程的情况下会产生什么样的问题呢?
JDK7线程不安全问题(死锁/数据丢失)
在JDK7中,HashMap容易出现线程不安全的问题点在于扩容时,创建新的数组,将旧数组中的数据重新进行hash,并将数据按照新的位置存放。
/**
* Transfers all entries from current table to newTable.
*/
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;
}
}
}
这段代码是HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。
下边是一个HashMap结构,其中存在hash冲突出现列表结构,1-2-3-4,现数组容量不足,将进行扩容:
扩容后的正确结果为:
假设现在有两个线程,A/B,线程A执行到newTable[i] = e;时CPU时间片耗尽被挂起,此时,e=2,next=3,e.next = NULL
线程A的时间片消耗完之后,线程B开始执行,并最终扩容完成。并将数据同步到了主内存中
此时线程A继续执行newTable[i] = e;结果为下图:
随后继续执行下一轮循环迁移,此时
3作为旧hashmap中的e,而在执行e.next时,从主内存中取到的结果是e.next= 2,此时3会继续按照头插法迁移到新的数组中去,因此新的一轮循环结果下来是旧的hashmap中,e=2:
再次执行循环,next = e.next = null表示执行完就结束了,继续进行数据迁移,把e=2迁移到新的数组中去,原本新hashmap中3->2,而本次循环执行的结果是2->3,此时就出现了相互引用,死循环了,而且其中数据4也丢失了。