Java并发编程系列-(5) Java并发容器 (7)

这节开始介绍并发容器,首先是ConcurrentHashMap,实现了线程安全的HashMap。之前也提到了HashMap在多线程环境下的问题,这小节先详细分析为什么HashMap多线程下不安全。

HashMap多线程环境下的问题分析

首先说结论,为什么HashMap不是线程安全的?在多线程下,会导致HashMap的Entry链表形成环形数据结构,一旦形成环形,Entry的next节点永远不为空,无论是进行resize还是get/size等操作时,就会产生死循环。

首先针对JDK7进行分析:

下面是resize部分的代码,这段代码将原HashMap中的元素依次移动到扩容后的HashMap中,

1: // Transfer method in java.util.HashMap - 2: // called to resize the hashmap 3: // 依次移动每个bucket中的元素到新的buckets中 4: for (int j = 0; j < src.length; j++) { 5: Entry e = src[j]; 6: if (e != null) { 7: src[j] = null; 8: do { // Next指向下一个需要移动的元素 9: Entry next = e.next; // 计算新Map中的位置 10: int i = indexFor(e.hash, newCapacity); // 插入到bucket中第一个位置 11: e.next = newTable[i]; 12: newTable[i] = e; // 指向原bucket中下一个位置的元素 13: e = next; 14: } while (e != null); 15: } 16: }

在正常单线程的情况下,如果有如下的HashMap的结构,为了方便这里只有2个bucket(java.util.HashMap中默认是 16)。

Screen Shot 2019-12-08 at 10.45.35 PM.png

按照上面的resize流程,e和next分别指向A和B,A是第一次迭代将会被移动的元素,B是下一个。

第一次迭代后,A被移动到新的Map中,Map的容量已经增大了一倍。A的位置如下图所示

Screen Shot 2019-12-08 at 10.46.40 PM.png

第二次迭代后,B被移动到了新的位置,如下图所示,C为下一个待移动的元素。

Screen Shot 2019-12-08 at 10.47.42 PM.png

第三次迭代之后,C被移动到了新的位置,由于C之后没有其他元素,因此整个resize过程完成,最后新的Map如下:

Screen Shot 2019-12-08 at 10.48.16 PM.png

在resize完成之后,每个bucket的深度变小了,达到了resize的目的。整个过程在单线程下没有任何问题,但是考虑到多线程的情况,就会可能会出现竞争。

现在有两个线程Thread1,Thread2同时进行resize的操作,假设Thread1在运行到第9行后,Thread2获取了CPU并且也开始执行resize的操作。

1: // Transfer method in java.util.HashMap - 2: // called to resize the hashmap 3: 4: for (int j = 0; j < src.length; j++) { 5: Entry e = src[j]; 6: if (e != null) { 7: src[j] = null; 8: do { 9: Entry next = e.next; // Thread1 STOPS RIGHT HERE 10: int i = indexFor(e.hash, newCapacity); 11: e.next = newTable[i]; 12: newTable[i] = e; 13: e = next; 14: } while (e != null); 15: } 16: }

Thread1运行后,对应的e1和next1别指向A和B,但是Thread1并没有移动元素。

Screen Shot 2019-12-08 at 10.49.53 PM.png

假设Thread2在获取CPU后完整的运行了整个resize,新的Map结构将会如下图所示:

Screen Shot 2019-12-08 at 10.50.42 PM.png

注意到e1和next1还是指向A和B,但是A和B的位置关系已经变了,按照resize的算法进行两轮迭代之后,变成如下的结构,

Screen Shot 2019-12-08 at 10.53.01 PM.png

Screen Shot 2019-12-08 at 10.53.29 PM.png

注意此时e和next的指向,在下一次的迭代中,将把A放在第3个bucket的一个位置,但是B仍然是指向A的,所以出现了下面的类似于双向链表的结构,

Screen Shot 2019-12-08 at 10.54.19 PM.png

接着Thread1就会进入到无限循环中,此时如果有get操作的话,也会出现无限循环的情况。这就是HashMap在多线程情况下容易出现的问题。

接着针对JDK8进行分析:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwzxpw.html