引言:
本文基于JDK1.8版本,而且对ConcurrentHashMap有一定了解的人,本文并非科普该类的用法,而是针对sizeCtl的含义做出纠正
科普文章推荐:
深入浅出ConcurrentHashMap1.8
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
sizeCtl 无可置疑是ConcurrentHashMap中一个重要的变量,在各种资料上能看到的基本就是
sizeCtl :默认为0,用来控制table的初始化和扩容操作
-1 代表table正在初始化
-N 表示有N-1个线程正在进行扩容操作
其余情况:
1、如果table未初始化,表示table需要初始化的大小。
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍
这些资料其实就是按照注释翻译了一遍,但其实注释是有错误的。
-N 表示有N-1个线程正在进行扩容操作,这句话是错误的。
这里-N的定义是有问题的,应该取-N对应的二进制的低16位数值为M,此时有M-1个线程进行扩容。
(感谢评论区的朋友指教)
证明:
能修改sizeCtl的方法有五个:
其中initTable是设为-1保证只初始化一次,而且初始化后,把sizeCtl设为长度的0.75倍,此时sizeCtl为正值。
而能把sizeCtl设置为-N的方法只有addCount跟tryPresize方法,而且两个方法的实现逻辑是很相似的,这里选用tryPresize作为讲解。
..省略部分代码
while ((sc = sizeCtl) >= 0) {
ConcurrentHashMap.Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
//表为空,初始化表
// 逻辑和initTable一样,完成后sc也是0.75倍的长度,为正数,继续循环
// 此处省略代码
}
//扩容大小没有达到阈值,或者超过最大容量,无法扩容,直接结束
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
//满足while循环条件,证明sc肯定>=0,所以一开始肯定不会满足该判断,而是执行else代码
// 这里的意义其实是已经有线程在进行扩容
if (sc < 0) {
ConcurrentHashMap.Node<K,V>[] nt;
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);
}
//设为-N的核心代码
//一开始是执行这个cas,也正是这里把cas设置为了-N
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
而设置为-N的关键也就是
int rs = resizeStamp(n);
(rs << RESIZE_STAMP_SHIFT) + 2
相关的方法跟常量:
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
private static int RESIZE_STAMP_BITS = 16;
//32-↑这个常量,其实就等于16
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
其中的 Integer.numberOfLeadingZeros(n),作用是获取最高位1的左边0的数量,因为n为int类型,所以这里返回值为[0,32]
比如 n=16 - 0001 0000 = 24,左边的0的数量就是 31-4 =27。
然后 或 215 , 就得到了rs。
所以rs 的取值范围其实是 [215,215+32]
再把 rs 带符号左移 16位 ,因为rs的 215位一定是 1,所以左移后 231位一定是1,即rs为负数。
根据上述rs范围可知,左移后rs很小(指-N很小)
最后再 +2 ,也不会变为正数。这就是sizeCtl 更新后的值了。
测试代码:(n是数组长度,都是2的幂次)
static final int resizeStamp(int n) {
int zeroCount = Integer.numberOfLeadingZeros(n);
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
public static void main(String[] args) {
// <0 时溢出,结束
for(int n=1;n>0;n*=2){
int rs = resizeStamp(n);
int ans = (rs << RESIZE_STAMP_SHIFT) + 2;
System.out.println("n:"+n+" rs:"+rs+" ans:"+ans);
}
}
结果:
n:1 rs:32799 ans:-2145452030
n:2 rs:32798 ans:-2145517566
n:4 rs:32797 ans:-2145583102
n:8 rs:32796 ans:-2145648638
n:16 rs:32795 ans:-2145714174
n:32 rs:32794 ans:-2145779710
n:64 rs:32793 ans:-2145845246
n:128 rs:32792 ans:-2145910782
n:256 rs:32791 ans:-2145976318
n:512 rs:32790 ans:-2146041854
n:1024 rs:32789 ans:-2146107390
n:2048 rs:32788 ans:-2146172926
n:4096 rs:32787 ans:-2146238462
n:8192 rs:32786 ans:-2146303998
n:16384 rs:32785 ans:-2146369534
n:32768 rs:32784 ans:-2146435070
n:65536 rs:32783 ans:-2146500606
n:131072 rs:32782 ans:-2146566142
n:262144 rs:32781 ans:-2146631678
n:524288 rs:32780 ans:-2146697214
n:1048576 rs:32779 ans:-2146762750
n:2097152 rs:32778 ans:-2146828286
n:4194304 rs:32777 ans:-2146893822
n:8388608 rs:32776 ans:-2146959358
n:16777216 rs:32775 ans:-2147024894
n:33554432 rs:32774 ans:-2147090430
n:67108864 rs:32773 ans:-2147155966
n:134217728 rs:32772 ans:-2147221502
n:268435456 rs:32771 ans:-2147287038
n:536870912 rs:32770 ans:-2147352574
n:1073741824 rs:32769 ans:-2147418110
可见,无论是对哪个长度,得到的sizeCtl新值都是很小的。
显然,若按注释理解:有N-1个线程,N这么大,这么多的线程,是不可能的。
重点来了 :
rs在左移了16位后,低16位全为0。
若只取低16位只看数值为M,初始为0。(其实按二进制看也可以,因为不可能有215这么多的线程在进行扩容,但为了说法严谨,还是只看数值)
+2后,M=2 。此时扩容线程为 M-1 = 1。 这样就能对应上注释的定义了
当然,若要给高16位找个意义,那就是它代表了当前容量n ,但并不是说数值等于容量大小,因为高16位是对n计算得出的
其实上面的证明已经足够了,但我们还能从如下几个地方说明错误。(科学严谨!!)
因为putVal时,若检测到当前节点hash值为-1,说明当前处于拓展状态,会把当前put线程加入到扩容的中
if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
而helpTransfer方法中,增加线程数量的代码是:
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
满足while条件,证明当前sizeCtl<0 ,而增加扩容线程数量的操作却是sc+1 ,而不是sc-1.
-N+1,N值只会变小,不满足N-1个线程的说法
但如果只看低16位的数值M,刚好就是对应数值+1,即扩容线程+1
除本方法外,tryPresize、addCount也有类似的代码,此处还是以tryPresize为例
//已经有线程在进行扩容
if (sc < 0) {
ConcurrentHashMap.Node<K, V>[] nt;
//判断扩容是否已完成
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);
}
可以看到一样是sc+1
判断整个扩容是否已完成
以及判断当前线程扩容任务是否完成
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//整个扩容已完成
if (finishing) {
//清空临时变量,更新全局变量
nextTable = null;
table = nextTab;
//并把sizeCtl设为1.5的n(旧长度),即还是0.75倍的数组长度
//此处把sizeCtl置为了正值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//整个扩容未完成,但当前线程扩容任务已完成,减少扩容线程数量
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//这里的代码一一对应上文置为-N的操作
//若sc的值已更新,证明有别的线程完成了整个扩容任务
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // 重新检查一遍结点
}
}
其中减少线程数量代码是sc-1,也是对不上注释的说法的。
至此,得出结论:
-N需要取对应的二进制的低16位数值为M,代表此时有M-1个扩容线程。而并不代表有N-1个线程
本文并无发现类似的观点,也没有参考到相关资料。
因此若有误,欢迎指出。