源码阅读系列JDK 8 ConcurrentHashMap 源码分析之 由transfer引发的bug

Posted christmad

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码阅读系列JDK 8 ConcurrentHashMap 源码分析之 由transfer引发的bug相关的知识,希望对你有一定的参考价值。

不阅读源码就不会发现这个事儿

前段时间在阅读ConcurrentHashMap源码,版本JDK 8,目前源码研究已经告一段落。感谢鲁道的ConcurrentHashMap源码分析文章,读到文章,感觉和作者发生了一些交流,解答了很多疑惑,也验证了一些想法。鲁道在简书的addCount分析文章点这里 (文章底部的评论中就有这篇文章发酵的原由)。鲁道还有其他ConcurrentHashMap源码分析的系列文章,在简书、掘金都有分布,感兴趣的同学可以进一步追踪。

推完文章,回到本篇的主题“阅读源码”,期间发生了一件有意思的事情,而且既然是个BUG,就提出来让更多人知道。

 

ConcurrentHashMap源码分析导读

ConcurrentHashMap的源码据说在 1.8 发生了巨大改变。并发put时,ConcurrentHashMap只会用sync锁住桶节点(我把table[index] 位置的节点称为 桶节点),并发度就是hash数组长度。在并发扩容时,每个线程可以一次转移一个分片区域的桶节点,互不干扰,详见transfer源码 变量 stride ,当然stride最小是16,所以桶不够的时候,是不会有那么多线程都在“并发转移”的。每个线程转移节点时是从后往前,也就是从下标大的节点往下标小的节点方向来处理转移,处理完一段分片后,领取下一段,整个旧table处理进度由ConcurrentHashMap#transferIndex属性控制,它是volatile修饰的,提供更好的可见性。

 

通过研读transfer相关的源码,知道了在 addCount方法中,第一条进去扩容的节点会把 sizeCtl 设为 rs << RESIZE_STAMP_SHIFT) + 2:(MARK-1)

代码片段:

else if (U.compareAndSwapInt(this, SIZECTL, sc,
                             (rs << RESIZE_STAMP_SHIFT) + 2))

rs的计算方式如下:

int rs = resizeStamp(n);

这个 rs会根据数组长度 n 为 2的多少次幂来进行变化,也就是table长度的一个标识符,取值范围在 32768~32799 之间。32678是 2^15 等于二进制 1000 0000 0000 0000 。

 

好了,现在我们已经接近问题现场。根据上面的 MARK-1,第一条线程扩容开始后,sizeCtrl已经是一个负数,而在addCount中你会发现在 MARK-1 的代码上面还有这么一段代码:

if (sc < 0) {
    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
        transferIndex <= 0)
        break;
    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
        transfer(tab, nt);
}

sc < 0 是因为发生扩容,sizeCtl已经为负数,那么上面这段代码中  一个负数 sc 如何能与  rs + 1 (rs正数) 和 rs + MAX_RESIZES (结果也是正数)两个都为正数 进行等值判断呢???而且 rs + 1 和 rs + MAX_RESIZES 也不是int溢出附近的值。当时是怎么也想不通负数如何与正数进行比较的,持着怀疑态度我去测试了resizeStamp方法,于是才有前文中我对 rs 取值的验证。

我的博客后面也会更新自己阅读ConcurrentHashMap源码时的一些收获,尽量把过程和结果输出出来。

 

无巧不成书

有意思的是,在上个月底那段时间阅读源码碰到这个问题时,开始了各种google,在StackOverFlow上正好有一位同学发表了疑似JDK 8 ConcurrentHashMap的BUG,追踪进去后,发现oracle已经采纳了该BUG。BUG链接。而正好就是这位同学,也在鲁道的简书文章下评论了。就是这么巧。相比提出BUG的这位同学,我的动手能力还有待提高......

终于,最后搞明白了真的就是代码写得有问题。该问题还存在于和transfer相关联的方法,只要是调了 transfer的,如addCount、helpTransfer、tryPresize等方法都有一样的BUG。

正确写法本文这里就不贴出了,相信大家思考一下就能得出结论。BUG清单中也有正解供参考。

 

总结

阅读优秀源码时,敢于质疑,敢于提出猜想,最后用事实去验证自己的猜想。









以上是关于源码阅读系列JDK 8 ConcurrentHashMap 源码分析之 由transfer引发的bug的主要内容,如果未能解决你的问题,请参考以下文章

JDK1.8之ConcurrentHashMap

Java Jdk1.8 HashMap源码阅读笔记一

java基础系列之ConcurrentHashMap源码分析(基于jdk1.8)

JDK1.8源码分析03之idea搭建源码阅读环境

源码阅读Java集合 - ArrayList深度源码解读

JDK1.8源码分析02之阅读源码顺序