每次扩容前,会调用resizeStamp函数以table容量为种子生成一个唯一的扩容戳。
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
// Integer.numberOfLeadingZeros 表示参数n的二进制表示,从左边开始算起,连续为0的个数
}
在扩容时,sizeCtl变量会用来表示扩容状态,高16位保存着扩容戳,低16位保存着并发扩容的线程数
2.addCount方法分析在putVal方法中,插入节点完毕后,调用addCount(1L, binCount),更新Map元素计数,其中含有扩容判断。Map元素总数超过阈值(0.75* table容量)则触发扩容。
扩容函数:
addCount()中扩容部分:
<1>处代码分析:检验当前线程是否可以参与扩容
-
(sc >>> RESIZE_STAMP_SHIFT) != rs
如果不相等,说明table容量已经变化,扩容以及完成,此时sizeCtl为扩容阈值
-
sc == rs + 1 (此处为jdk1.8中的bug,已在jdk12以后修改)
用于判读扩容结束,(rs << 16) + 1表示扩容结束,+2表示第一个线程进行扩容,若第一个线程扩容结束,会讲rs进行-1操作,此时sc == (rs << 16) +1
-
sc == rs + MAX_RESIZERS (此处为jdk1.8中的bug,已在jdk12以后修改)
此初应该是sc == (rs << 16) + MAX_RESIZERS ,MAX_RESIZERS默认为( 1<<16 ) - 1
扩容线程数量已达到最大值
-
(nt = nextTable) == null
此处表示nextTable还未完成初始化(只能有一个线程去完成)
-
transferIndex <= 0
表示需要迁移的桶已经被并发扩容线程瓜分完毕,无法继续帮助扩容
此处addCount以及helpTransfer中 int rs = resizeStamp(n); 赋值逻辑存在问题(JDK1.8中),rs获取的扩容戳为低16位数据,与sc进行比较时,需要左移16位才能正确判断之后的 sc == rs + 1以及 sc == rs + MAX_RESIZERS
该Bug已在JDK12中被修复,见下面的链接。
[Bug ID: JDK-8214427 probable bug in logic of ConcurrentHashMap.addCount()]
在stackoverflow中也找到了该问题的讨论。
stackoverflow.com
这个bug是2020年才在JDK12中修改的,对程序影响估计也很小,国内这么多公司都在用JDK1.8,也没出现什么大问题,因此也会很久没有修改过来吧,但是大家阅读源码过程中还是应该去看新版本JDK12以上的,此处已经修改过来了。
若要复现此bug,需要做到并发扩容线程达到最大值时进行判断,很难去模拟该情况(电脑跑到那么多线程确实也很离谱,默认情况下MAX_RESIZERS为 (1<< 16)-1 ),以及判断当前扩容是否结束,很难模拟一个线程刚好允许到此处,刚好遇到了sc == (rs << 16) +1 的情况。
JDK8中源码如下:addCount方法:
//检查是否要进行扩容,插入元素在putVal方法中总会进行检查
if (check >= 0) {
Node[] tab, nt; int n, sc;
//此处sizeCtl表示的是扩容阈值,s的值为之前sumCount()计算得到的,表示当前总KV对个数
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
//此处rs的赋值存在bug
int rs = resizeStamp(n);
//int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT; //生成当前容量n对应的扩容戳
if (sc < 0) { // 并发扩容已经开始进行
// <1> flag
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0) // 检验当前线程是否可以参与扩容
break;
// 通过检验,update sizectl -> sizectl+1,表示多了一个线程参与扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// sc >= 0 说明当前容器还未进行并发扩容,运行此处的线程是第一个扩容线程
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2)) // rs << 16 + 1表示扩容结束
// 此处+2 表示第一个线程进行扩容,更新sizeCtl变量从而修改扩容状态
transfer(tab, null);
s = sumCount();
}
}



