不阅读源码就不会发现这个事儿
前段时间在阅读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清单中也有正解供参考。
总结
阅读优秀源码时,敢于质疑,敢于提出猜想,最后用事实去验证自己的猜想。
原文地址:https://www.cnblogs.com/christmad/p/10123390.html