Java并发进阶系列:深度讨论官方关于jdk1.8ConcurrentHashMap的resizeStamp源代码修复逻辑

📅 2026/7/28 11:32:35 👁️ 阅读次数 📝 编程学习
Java并发进阶系列:深度讨论官方关于jdk1.8ConcurrentHashMap的resizeStamp源代码修复逻辑
前言

首先给出以下open JDK版本的序号说明和Oracle JDK序号说明

(1)对于JDK8或者Java 8

即可指代openjdk-8-jdk或者java-1.8.0-openjdk,

也可指代Oracle家的Java SE 8或者JDK 8u211 and later

(1)对于JDK16或者Java 16

即可指代openjdk的JDK 16.0.2 ,也可指代Oracle家的Java SE 16或者 jdk16.0.1,这里为何给出Java 8和Java 16版本说明?

首先resizeStamp的bug在Java8出现,并在Java 12被修复,因此本文直接给出最新版Java 16作为bug修复前后对比即可。

以下做个约定:统一以Java X形式作为版本称号,CHM:ConcurrentHashMap的简称,以此减少阅读障碍。

在前面的文章中,关于Java 8 的CHM addCount方法里面分支2:resizeStampsc==rs+1、sc==rs+MAX_RESIZERS的讨论中,已经指出其bug嫌疑:

privatefinalvoidaddCount(longx,intcheck){// 分支1 省略...// 分支2if(check>=0){Node<K,V>[]tab,nt;intn,sc;while(s>=(long)(sc=sizeCtl)&&(tab=table)!=null&&(n=tab.length)<MAXIMUM_CAPACITY){intrs=resizeStamp(n);// 注意这里计算出的rs是正值if(sc<0){// sc是负值,怎么会等于rs+1或者rs + MAX_RESIZERS这个正值呢? 有可能是个bugif((sc>>>RESIZE_STAMP_SHIFT)!=rs||sc==rs+1||sc==rs+MAX_RESIZERS||(nt=nextTable)==null||transferIndex<=0)break;if(U.compareAndSetInt(this,SIZECTL,sc,sc+1))transfer(tab,nt);}elseif(U.compareAndSetInt(this,SIZECTL,sc,(rs<<RESIZE_STAMP_SHIFT)+2))transfer(tab,null);s=sumCount();}}
官方bug描述

其实这个bug在open jdk的官方bugs主页已经给出相关解释和修复过程,链接:官方bug描述页面:

从Detail这一块描述得到信息如下:

bug的描述:ConcurrentHashMap.addCount()设计逻辑中可能存在bug。(这里虽然提到addCount()方法,但本人更想强调的是扩容分支的resizeStamp的bug)

级别是:bug

当前状态:已经修复

影响的版本:Java 11、Java12

在哪个版本得到修复:Java 12

使用操作系统平台:所有

bug页面创建时间:2018-11-26

解决bug的最后时间:2018-12-11

bug所属库:Java的核心库——core-libs

提交者的修复建议

In the above code, condition of (sc == rs + 1 || sc == rs + MAX_RESIZERS ) would never be true , since the value of rs is positive and the value of sc is negative .

译:条件 (sc == rs + 1 || sc == rs + MAX_RESIZERS )永远不可能true,因为rs的值为正数,而sc值为负数

并建议修改为:

The correct condition should be (sc >>> RESIZE_STAMP_SHIFT) == rs + 1 || (sc >>> RESIZE_STAMP_SHIFT) == rs + MAX_RESIZERS, which can be used to dedect if resizing process finished or resizing threads reaches maxmium limitation

译:正常的条件应该是这样: (sc >>> RESIZE_STAMP_SHIFT) == rs + 1 || (sc >>> RESIZE_STAMP_SHIFT) == rs + MAX_RESIZERS,这两个条件表示扩容任务已结束或者参与扩容的线程总数达到最大值

确实,这个bug非常明显,以分支2作为说明

int rs = resizeStamp(n),以容量n=16作为说明,rs计算为下面的值

0000 0000 0000 0000 1000 0000 0001 1011

考察低16位,rs+1的结果显然是一个正数

0000 0000 0000 0000 1000 0000 0001 1100

rs+MAX_RESIZERS同理也是一个正数,接着判断条件if(sc<0)成立才能进入rs+1等条件,也即此时sc是一个负数(其实是因为首个扩容线程会将sc设为(rs << RESIZE_STAMP_SHIFT) + 2的一个基础负数),基于此,有提交者在这里发现的了bug:sc是负数,而rs+1是正数,因此sc==rs+1永远不会成立

官方关于此bug的讨论过程

JCP JSR-166 Expert Group (关于Java并发编程的规范提案的专家组)几个相关成员的对话过程即可知道他们对问题的思考和处理方式。

在Activity这个栏目就是用于提交者已经相关专家bug讨论过程,“All”是显示所有他们的活动记录,一般无需关注,“Comments”显示他们的对话过程,bug的讨论过程就在这里,因此需要重点关注,具体如下:

以下是来自“Comments”区域的内容:

(最开始由Webbug Group 这个小组提交了该issue - 2018-11-26 00:53)

Stuart Marks 说:

Stuart Marks added a comment - 2018-11-28 09:10

Martin, can you take a look at this?

Martin,来,帮我看看这个bug?

Martin的回答,主要意思是:bug提交者在查一个确实是由addCount产生错误计数,但Martin说他们也没有可以使用的压测案例,并建议使用者用多线程做压测来让addCount的这个bug复现,但这个bug不好复现。

Resizing the internal bucket array is hairy race-prone code, and hard to stress test because resizes are relatively rare.

The reporter probably investigated an actual occurrence of incorrect count (we could ask!), but we don’t have a stress test reproduction that could be used.

One should be able to construct a stress test using multiple threads to trigger concurrent attempts to addCount, but it won’t be easy.

David Holmes 对Martin说:

What is your analysis just based on the code and the report? It certainly appears incorrect to me.

你的分析只是基于代码以及提交的报告?这个bug在我看来显然是不正确的。

Martin回答David Holmes :

Martin说自己也看了看源码但研究时间不够长,自己还没能搞懂其设计,然后说Doug应该记得这个设计!

I stared at the code for a while, but not long enough to understand it. Doug will remember!

Doug Lea added a comment - 2018-11-28 15:53:

Doug Lea看到这个bug,做了基本的分析:这个bug会影响到CHM性能也即有些线程不能参与到扩容任务中,并指出这个bug只是影响性能而不是一个引起map发生错误的bug,指出这个bug需要修复。

Yes. Some of this check now includes dead code, because of a change of representation at one point that wasn’t adjusted for. With the possible effect of some threads not helping resize (a performance, not map correctness bug) This should be fixed (and is committed in jdr166 repo):

然后他贴出修复前后的源码diff

---ConcurrentHashMap.java.~1.314.~2018-10-0513:42:39.860409607-0400+++ConcurrentHashMap.java2018-11-2818:48:55.998082379-0500@@-2307,9+2307,9@@(n=tab.length)<MAXIMUM_CAPACITY){intrs=resizeStamp(n);if(sc<0){-if((sc>>>RESIZE_STAMP_SHIFT