源码阅读 - ConcurrentHashMap#addCount 方法里面的 bug
去年底重撸了部分 ConcurrentHashMap 源码,当时笔记为 word 形式,比较乱,且刚好当时入职了一家新公司,整理这部分就停下来了(源码学习这部分在大部分公司里都会没时间去做,时间全靠挤)。刚好最近读完部分 redis 内部数据结构实现(虽然 C 语言不是很懂,但应该还是读懂了重要的部分),正好与Java 这边的 ConcurrentHashMap 形成对比,CHM 扩容这块的源码之后重新整理下就会发上来。
为了见证自己曾经撸过 CHM 源码,并且发现代码的怪异之处,现在将一些“证据”整理发上来,作为后续 CHM 源码学习笔记的一个起点......
addCount 方法中出 bug 的代码一如下:
1 if (check >= 0) { 2 Node<K,V>[] tab, nt; int n, sc; 3 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && 4 (n = tab.length) < MAXIMUM_CAPACITY) { 5 int rs = resizeStamp(n); 6 if (sc < 0) { 7 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || 8 sc == rs + MAX_RESIZERS || (nt = nextTable) == null || 9 transferIndex <= 0) 10 break; 11 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) 12 transfer(tab, nt); 13 } 14 else if (U.compareAndSwapInt(this, SIZECTL, sc, 15 (rs << RESIZE_STAMP_SHIFT) + 2)) 16 transfer(tab, null); 17 s = sumCount(); 18 } 19 }
出 bug 的代码二定位于 if (sc < 0) 分支下的第一个 if 条件:
1 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || 2 sc == rs + MAX_RESIZERS || (nt = nextTable) == null || 3 transferIndex <= 0) 4 break;
rs = resizeStamp(n); 这个方法的作用就是对散列表的数组的长度进行标记,其输出如下:
resize stamp when n=0 : 32800 resize stamp when n=1 : 32799 resize stamp when n=2 : 32798 resize stamp when n=3 : 32798 resize stamp when n=4 : 32797 resize stamp when n=5 : 32797 resize stamp when n=6 : 32797 resize stamp when n=7 : 32797 resize stamp when n=8 : 32796 resize stamp when n=16 : 32795 resize stamp when n=32 : 32794 resize stamp when n=1<<30 : 32769 resize stamp when n=1<<31 : 32768 resize stamp when n=1<<32 : 32799 resize stamp when n=1<<33 : 32798
可以发现 resizeStamp 方法的输出是固定在 32768~32798 之间的值(n=0和1对应的32799、32800都不会是CHM内部数组的长度值)
32768 的二进制为: 1000 0000 0000 0000,1 个 1 后面跟着 15 个 0,总计 16 位
我们取 n = 4 来分析(相当于创建CHM时指定了 initialCap),n=4 时 rs = 32797 = 32768 + 29, 对应二进制为:1000 0000 0001 1101
在“代码一”中有一个 else if 代码分支,其对应的是第一个进入扩容的线程执行的操作:第一条线程扩容时,首先将原本是正数的 sizeCtl,修改为负数,对应操作:
U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2)
RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS = 16,
CAS操作后 sizeCtl = 1000 0000 0001 1101 0000 0000 0000 0010,对应十进制为:-2145583102(32797<<16=-2145583104),总之肯定是个负数,且 sizeCtl 为负数时其高 16 位保存的是扩容前数组的 sizeStamp。
将出bug的代码二再贴一遍:
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
能进入上述代码,sc < 0,rs 是正数且取值范围为 32768~32798,因此不可能出现上述代码中的“sc == rs + 1” 和 “sc == rs + MAX_RESIZERS”(注:MAX_RESIZERS=65535,是帮助扩容的最大线程数限制)。
而前几行我们分析过 “sizeCtl 为负数时其高 16 位保存的是扩容前数组的 sizeStamp”,因此这两处 bug 正确写法应该是 sc == rs<<16 + 1 和 sc == rs<<16 + MAX_RESIZERS
当时比较有意思的是,在参考 https://juejin.im/post/5b001639f265da0b8f62d0f8#comment 这篇 CHM 总结文章分析到 addCount 时发现这个疑虑,搜索 stackoverflow 时有人也提出了同样的问题,但这位同学更进了一步,直接向 JDK 提出了 BUG,JDK bug link:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427。最后这个 BUG 被 JDK 收录。详情可以在 bug link 中查看。
这位同学提出 bug 的同时,对 bug 的纠正用的是 <<< ,而我在这篇文章中用的是 <<,写的时候突然意识到这一点不同,用编译器(Intellij Idea)验证了一下,java 中没有 <<< 这个符号,只有 >>> 。不过这点倒是无伤大雅,整体的思路对了我觉得就达到目的了,一些小细节的地方硬记它有还是没有作用不大。而且你只需要有一款像我一样的编译器,就能提示你 <<< 是错误的,这已经能解决问题。
好了,今天这个 bug 的分析到此到一段落,后续会将 CHM 几个重要方法的分析贴上来,不过我应该还是会继续用脑图导出的图片的方式,可能是一个方法做一张图,内容的框架和鲁道大佬总结的这篇 CHM 应该差不多:https://juejin.im/post/5b001639f265da0b8f62d0f8#comment,没学习的小伙伴赶紧去看一下吧~