大家好,我是Skow,看到标题第一反应是不是?JDK8的ConcurrentHashMap怎么会有bug呢?不影响使用,在JDK12已经修复。在这个JDKBug收集论坛中,我们可以看到这个报告指出ConcurrentHsahMap在执行addCount()时会出现问题。我们先来看看具体情况。该方法在JDK8和JDK12中的体现JDK8privatefinalvoidaddCount(longx,intcheck){//...if(check>=0){Node[]tab,nt;诠释n,sc;while(s>=(long)(sc=sizeCtl)&&(tab=table)!=null&&(n=tab.length)>>RESIZE_STAMP_SHIFT)!=rs||sc==rs+1||sc==rs+MAX_RESIZERS||(nt=nextTable)==null||transferIndex<=0)中断;//...}//...}}}JDK12privatefinalvoidaddCount(longx,intcheck){//...if(check>=0){Node[]tab,nt;诠释n,sc;while(s>=(long)(sc=sizeCtl)&&(tab=table)!=null&&(n=tab.length)>>RESIZE_STAMP_SHIFT)!=rs的判断,所以问题出现,这种情况有什么问题?为什么像DougLea这样一丝不苟的人一开始就没有想过呢?欢迎阅读今天的文章,揭秘这个BUG的前世今生。熟悉ConcurrentHashMap展开和计数逻辑的同学可以直接跳到第三部分看问题分析。基本定义默认小伙伴对ConcurrentHashMap有了初步的了解,以及CAS算法等基础知识。本文不展开解释。要了解ConcurrentHashMap中的扩容和计数逻辑,我们需要先了解一些定义,或者直接开始阅读源码。我们会有点吃力。接下来,我们将列出一些将在我们的addCount方法中使用的基本定义。我们让我们先熟悉一下。sizeCtlsizeCtl的定义有点复杂,但也很重要。初始化数组sizeCtl=0默认状态表示数组还没有初始化sizeCtl>0记录下一次需要扩展的counterCells计数单元的大小,不为空时,大小通常为2的次方RESIZE_STAMP_SHIFT=32-RESIZE_STAMP_BITS用于生成记录扩展MAX_RESIZERS=(1<<(32-RESIZE_STAMP_BITS))-1辅助扩展的最大线程数transferIndex扩展任务分配的进度标记。初始值为n,按倒序扩容。位置每减少一步,最后减少到<=0,即完成整个扩容任务的分配。考虑?第一反应是用volatile还是synchroieed来保证数据添加的准确性。有的同学可能会想到用volatile修饰变量,然后用CAS加。性能会不会更好?但是在并发密集的情况下,CAS也可能会导致部分线程一直在自旋。为了避免这种极端的并发情况,DougLea采用了分而治之的思想,尽可能避免这种竞争。也就是使用baseCount+CounterCellbaseCount,顾名思义,就是一个基本的计数变量,counterCells,我们上面有简单的介绍。它是一个计数单位。不为空时,size一般是2的次方。没有竞争时,cas修改会直接修改baseCount。如果CAS修改失败,则不会进行自旋,而是以hashmap的形式获取一个数组下标在counterCells中的位置进行计数。如果还是找不到对应的合适位置,就会继续重复这个操作。多次失败后,它会针对counterCells进行扩容,继续不知疲倦地寻找合适的格子进行计数。了解了addCount的大致设计思路后,再来看源码如果已经调整大小,帮助*在工作可用时执行转移。在传输后重新检查占用*以查看是否已经需要另一个调整大小*因为调整大小是滞后的添加。**@paramx要添加的计数*@param检查如果<0,不检查调整大小,如果<=1只检查是否无竞争*/privatefinalvoidaddCount(longx,intcheck){CounterCell[]as;长b,s;//这里的compareAndSwapxxx是一个CAS操作//也就是说如果counterCells不为null,则直接在counterCells中操作,否则直接操作baseCountif((as=counterCells)!=null||!U.compareAndSwapLong(this,BASECOUNT,b=baseCount,s=b+x)){CounterCella;长v;诠释米;//默认线程安全,无竞争booleanuncontended=true;if(as==null||(m=as.length-1)<0||//ThreadLocalRandom.getProbe()获取当前线程的哈希值//compareAndSwapLong要做的事情是找到一个网格不为null,执行value+x,如果CAS失败则进入fullAddCount(a=as[ThreadLocalRandom.getProbe()&m])==null||!(uncontended=U.compareAndSwapLong(a,CELLVALUE,v=a.value,v+x))){fullAddCount(x,无竞争);返回;}如果(检查<=1)返回;s=sumCount();}//展开代码暂略其实这个方法就是尽可能保证计数成功。里面的逻辑就不解释了。这是一堆iflese。有兴趣的朋友可以进一步阅读第二点。这里check方法判断check的值,我们可以理解为链表的长度,根据不同的调用会发生变化。如果我们在put方法中调用addCount,那么check必须大于等于2。如果是在computeIfAbsent方法中调用,那么check可能为1,如果<=1,则不会进行扩容检查操作,而且也是为了保证性能,所以直接返回展开逻辑。逻辑上,我们可以理解这个bug的本质。ConcurrentHashMap的扩展逻辑,我认为是ConcurrentHashMap的核心逻辑之一。让我们仔细看看。privatefinalvoidaddCount(longx,intcheck){//只有当从外部传入的check大于等于0时,才会对扩展进行检查if(check>=0){Node[]选项卡,nt;诠释n,sc;//sizeCtl会被赋值给下一次扩容的大小,这里比较s和下一次扩容的大小//tab没有变化,数组长度没有达到阈值while(s>=(long)(sc=sizeCtl)&&(tab=table)!=null&&(n=tab.length)>>RESIZE_STAMP_SHIFT)!=rs||sc==rs+1||sc==rs+MAX_RESIZERS||(nt=下表)==空||transferIndex<=0)中断;如果(U.compareAndSwapInt(this,SIZECTL,sc,sc+1))转移(tab,nt);}elseif(U.compareAndSwapInt(this,SIZECTL,sc,(rs<=0){Node[]tab,nt;诠释n,sc;while(s>=(long)(sc=sizeCtl)&&(tab=table)!=null&&(n=tab.length)>>RESIZE_STAMP_SHIFT)!=rs||sc==rs+1||sc==rs+MAX_RESIZERS||(nt=nextTable)==null||ferIndex<=0)中断;如果(U.compareAndSwapInt(this,SIZECTL,sc,sc+1))转移(tab,nt);}elseif(U.compareAndSwapInt(this,SIZECTL,sc,(rs<>>RESIZE_STAMP_SHIFT)!=rs这个条件是也是我们的错误。我们把这个条件理解为sc右移16位,判断是否不等于我们的rssc=sizeCtl,即扩容完成后,sc会被赋值扩容数组rs的大小,正好now就是我们分析的扩容时间戳,当进入这个if判断的时候,肯定是说我们的tab发生了变化,那么肯定有另外一个线程在扩容ConcurrentHashMap,或者在扩容的时候已经结束了(nt=nextTable)==null可能是trueprivatefinalvoidtransfer(Node[]tab,Node[]nextTab){...if(finising){nextTable=null;//在“nextTable=null”之后表发生了变化table=nextTab;sizeCtl=(n<<1)-(n>>>1);返回;}...}当扩容结束后,这个sizeCtl发生了变化,与当前线程获取到的sizeCtl不匹配,即(sc>>>RESIZE_STAMP_SHIFT)!=rs这个条件一直为假,(sc==rs+1||sc==rs+MAX_RESIZERS)永远不会成立,扩展线程数永远不会被限制。那么,这个bug会不会影响我们的使用呢?当这个条件判断为假时,进入第二个if,又是一个cas判断。这个cas判断会失败,当前线程会在下一次迭代中跳出while循环或者继续尝试帮助扩容。虽然设计思路略有偏差,但好在不影响实际使用。总结至此,我们的分析就结束了。这个bug比较隐蔽,但是在了解了ConcurrentHashMap的扩展逻辑之后,我们还是可以了解到bug的产生。我们简单总结为judgement扩容时的条件判断写错了,导致扩容的线程数不受控制,但是不影响实际使用,因为在真正的扩容逻辑传递方式中,参与扩容的线程也会发现扩容已经结束,不会参与扩容即本文不分析真正的转账逻辑,有兴趣的同学可以阅读