《Java困惑》:多并发情况下HashMap是否还会产生死循环
ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现,
在看很多博客在介绍ConcurrentHashMap之前,都说HashMap适用于单线程访问,这是因为HashMap的所有方法都没有进行锁同步,因此是线程不安全的,不仅如此,当多线程访问的时候还容易产生死循环。
虽然自己在前几天的时候看过HashMap的源码,感觉思路啥啥的都还清楚,对于多线程访问只知道HashMap是线程不安全的,但是不知道HashMap在多线程并发的情况下会产生死循环呢,为什么会产生,何种情况下才会产生死循环呢???
正由于有这个问题,于是先将分析ConcurrentHashMap的源码的事情给放了一放,开始在网上查找这个问题的原因。
声明:
由于上面博客中所分析的HashMap的源码和我目前所看到的源码不一致,发现改动挺大的。目前我的JDK的版本是:jdk1.8.0_45.
这篇博文想讨论的问题是:在目前jdk1.8.0_45源码下,还存不存在上面列出的博文中所将到的这种死循环的问题,个人的答案是:不存在了。若有错,请批评指正
原HashMap的源码产生死循环的过程
下面贴出原HashMap的源码,这是原博客分析的基础,因此,截图如下:
第一篇博客中所贴出的HashMap的源码如下:
个人小结:根据原HashMap的源码,当我们想往HashMap中添加某个元素<K,V>
时,假如根据k的hash值找到的存储位置是在table中的index,且刚好此位置已经有了元素,即发生了碰撞,碰撞的处理方式为:将此元素加在此位置的链表的头部,注意,是加在链表头部。
在addEntry方法的代码中很容易的看出这一点。而addEntry方法是在put方法中检测该key在该位置不存在时调用的。
addEntry方法代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
就因为是将结点加在链表的头部,所以就出现了原博文中介绍的死循环问题。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
死循环的产生具体过程描述如下(图解可以看原博文):
第一步:假设线程1刚记录下来e=3,next=7 就切换到线程2执行
第二步:线程2执行完resize的全过程,结果如下:table[3]—>7——>3——>null.
第三步:现在又切换到线程1继续执行,会进行如下三轮循环
1)第一轮循环,e = 3,next = 7,由于线程1和线程2的newTab是独立的,因此,此轮循环的结果为:newTab[3]—->3—->null;
2)第二轮循环,e = 7,next = 3(此next就是节点e=7的下一个节点,在线程2中改变的),因此,结果为:newTab[3]—–>7—->3—->null.
3)第三轮循环,e = 3,next = null(此next就是此时节点e=3的下一个节点,为null,在第一轮循环中改变的),因此,结果为:
newTab[3]—->3<———>7(这里的双箭头表示节点3的下一个节点为节点7,节点7的下一个节点为3)。就这样循环就产生了。
当下次我们用get方法来获取get(key)方法来获取此可以与节点3和7相同的hash值的value时就进行了死循环中。
现在HashMap源码中不会产生上面介绍的死循环
为避免误会,特此声明,JDK版本为:jdk1.8.0_45
HashMap源码中的put方法的源码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
resize方法的代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
在上面的代码中(//——-//位置),我们可以看到现在的HashMap是将新节点加在了链表的最后面,而不是最前面。
因此,如果按照现在HashMap的源码的思路,则正常的Rehash过程如下(只需看第三步,第二步也应该改下顺序):
现在我们把上面产生死循环的例子搬移到这里来看是否还会产生死循环呢??
过程描述如下:
第一步:假设线程1刚记录下来e=3,next=7 就切换到线程2执行
第二步:线程2执行完resize的全过程,结果如下:table[3]—>3——>7——>null.
第三步:现在又切换到线程1继续执行,会进行如下2轮循环结束
1)第一轮循环,e = 3,next = 7,由于线程1和线程2的newTab是独立的,因此,此轮循环的结果为:newTab[3]—->3—->null;
2)第二轮循环,e = 7,next = null(此next为null,在线程2中改变的),因此,结果为:newTab[3]—–>3—->7—->null.
就这样结束了,正常运行,不会产生死循环。
Hashtable
最后要说的一点是:Hashtable的put方法的加入节点的方式是加入到链表的头结点。但是,Hashtable是线程安全的,更不会有这个问题
Hashtable的addEntry方法代码如下:
好了,这个代码算是比较正常的。而且没有什么问题。
并发下的Rehash
1)假设我们有两个线程。我用红色和浅蓝色标注了一下。
我们再回头看一下我们的 transfer代码中的这个细节:
1
2
3
4
5
6
7
|
do
{
Entry<K,V> next = e.next;
// <--假设线程一执行到这里就被调度挂起了
int
i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
while
(e !=
null
);
|
而我们的线程二执行完成了。于是我们有下面的这个样子。
注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。
2)线程一被调度回来执行。
- 先是执行 newTalbe[i] = e;
- 然后是e = next,导致了e指向了key(7),
- 而下一次循环的next = e.next导致了next指向了key(3)
3)一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。
4)环形链接出现。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。
于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。