ConcurrentHashMap的sizeCtl含义纠正

引言:
本文基于JDK1.8版本,而且对ConcurrentHashMap有一定了解的人,本文并非科普该类的用法,而是针对sizeCtl的含义做出纠正

科普文章推荐:
深入浅出ConcurrentHashMap1.8

sizeCtl定义及注释

  /**
     * 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的方法有五个:

  1. initTable()
  2. addCount()
  3. tryPresize()
  4. transfer()
  5. helpTransfer()

其中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计算得出的

反证

其实上面的证明已经足够了,但我们还能从如下几个地方说明错误。(科学严谨!!)

helpTransfer

因为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

transfer()

判断整个扩容是否已完成
以及判断当前线程扩容任务是否完成
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个线程

本文并无发现类似的观点,也没有参考到相关资料。
因此若有误,欢迎指出。

你可能感兴趣的:(源码,数据结构,笔记)