1. 扩容戳
每次扩容前,会调用resizeStamp函数以table容量为种子生成一个唯一的扩容戳。
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
在扩容时,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方法:
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
|