编程语言只是一种工具,它不应该成为我们技术前进之路上的壁垒。

源码阅读 - 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,没学习的小伙伴赶紧去看一下吧~

 

posted on 2019-08-20 23:30  独角没有戏  阅读(1409)  评论(2编辑  收藏  举报

导航