ConcurrentHashMap之transfer()扩容深入源码分析

引言:
首先“打个广告”,
对于sizeCtl变量,如果读者还认为取值-N 时代表有N-1个扩容线程(因为基本资料都是这么说的),
建议先阅读我的另一篇文章:
ConcurrentHashMap的sizeCtl含义纠正

引言:
看了很多资料,有说的好的,但感觉有些地方讲得不是很好,所以加入了自己的理解,写出了这篇博文,希望对各位有一点帮助。

精华分析

因为源码跟注释都很多,我先直接把精华的分析总结出来。

  1. transfer是支持并发扩容的,而且每个线程处理的桶区间是不相交的在这里插入图片描述
  2. for死循环确保整个扩容任务完成
  3. while (advance)循环中,负责分配各线程的处理桶区间,扩容时逆序扩容,即从高位到低位,而不像HashMap顺序扩容
  4. 对链表的处理中,使用的是新建结点的方式,而不修改原结点顺序,保证了在扩容期间,get操作也能正常使用且能返回正确结果
  5. 链表的拆分获得是一条逆序链表,而另一条链表顺序不定,而不是逆序/顺序链表 (当然指的是能拆分的一般情况)
  6. 处理完成需要放置ForwardingNode,作为占位符,表示已转移该字符

transfer()源码解析

ps:注释中所说关于扩容线程的增加、减少,并不是真的指数值增加/减少了,只是便于直观理解。

private final void transfer(ConcurrentHashMap.Node<K, V>[] tab, ConcurrentHashMap.Node<K, V>[] nextTab) {
        int n = tab.length, stride;
        // 根据CPU核心数,分配每个CPU处理的桶数量。
        // 目的是让每个 CPU 处理的桶一样多,避免出现转移任务不均匀的现象,
        // 如果桶较少的话,默认一个 CPU(即一个线程)处理 16 个桶
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;
        //nextTab未初始化,说明这是第一个扩容线程
        if (nextTab == null) {            // initiating
            try {
                //扩容两倍
                @SuppressWarnings("unchecked")
                ConcurrentHashMap.Node<K, V>[] nt = (ConcurrentHashMap.Node<K, V>[]) new ConcurrentHashMap.Node<?, ?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                //扩容时n<<1造成溢出,证明当前已无法扩容为2倍,直接设置为最大值
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            //更新全局变量
            nextTable = nextTab;
            //初始值为n,原数组长度
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //创建一个fwd结点,用于标记数组元素已被转移
        ConcurrentHashMap.ForwardingNode<K, V> fwd = new ConcurrentHashMap.ForwardingNode<K, V>(nextTab);
        //advance表示是否需要推进,标记当前桶是否已处理完毕
        //  true:需要再次推进一个下标(i--),
        //  false:不能推进下标,需要先处理当前的下标,才能继续推进
        boolean advance = true;
        //标记整个扩容过程是否已完成
        boolean finishing = false;
        //死循环,保证修改成功
        //i : 当前处理的桶下标  ; bound :当前线程可处理的桶边界
        for (int i = 0, bound = 0; ; ) {
            ConcurrentHashMap.Node<K, V> f;
            int fh;
            // 该循环用于控制 i 递减。此外,每个线程都会通过该循环取得自己需要转移的桶的区间
            // 之所以需要用循环控制,笔者认为主要是第三个if的CAS操作有失败的可能性
            while (advance) {
                int nextIndex, nextBound;
                // 对 i 减一,判断是否大于等于 bound
                // 1. 条件不成立,说明该线程当前已处理完成分配到的桶
                // 2. 否则说明线程任务未完成,设置为false,以便跳出循环,开始处理当前桶
                // 若finishing为true,即整个扩容已结束,自然无需推进
                if (--i >= bound || finishing)
                    advance = false;
                // 两个目的: 1. 更新nextIndex,因为transferIndex是volatile的,保证获取最新的转移下标
                // 若满足if,说明总扩容任务已经完成,设置i=-1,是为了跳出循环后,满足if (i < 0)
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // CAS修改transferIndex。  进入该if的情况
                //1.第一次进入循环,分配每个线程处理的桶的区间
                //2. 不满足上面两个情况,说明当前线程任务已完成,但总扩容任务还没完成,再次分配扩容任务
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    //更新桶边界以及当前桶下标

                    bound = nextBound;
                    // 注意,从i--以及这里的赋值,可以得知扩容是从高位到低位,即逆序处理,而HashMap中是顺序处理的
                    i = nextIndex - 1;
                    // 任务分配好了,跳出循环处理当前i
                    advance = false;
                }
            }
            // 笔者认为这里只有 i<0 的条件是有意义的,其余两个都无意义,
            // 因为i能被赋值的最大值是(nextIndex - 1)=(transferIndex -1)<=(n -1 ),因为transferIndex初始值为n,而且transferIndex与i都是只减不增,不可能达到其他两个条件
            // i<0,只对应上方while循环中的 i=-1 ,不会对应第一个if中的 --i>=bound
            // 因为bound = nextBound >=0 ,所以要满足if (--i >= bound),--i>=0必须成立,所以 i<0必不可能成立。而若不满足 1、2个if,就会如上文注释所说再次获取扩容任务
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //总扩容任务完成
                if (finishing) {
                    //置null, 保持nextTable只会在扩容时不为null的特性
                    nextTable = null;
                    //更新全局变量
                    table = nextTab;
                    //结果:1.5*n ,但别忘了n是旧数组长度,新数组长度为2n,而1.5n = 0.75 * 2n , 所以还是等于数组长度的0.75倍
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //总扩容任务未完成,但当前线程已完成,CAS减少扩容线程数量
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //这里看似迷的操作,其实是一一对应回addCount / TryPresize中把sizeCtl置为负值时的操作: U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //能到这个逻辑时,说明没有其他扩容线程了,即当前线程是最后的扩容线程
                    //总扩容已完成,更新finishing,advance设为true是为了能进入上述的while循环,而且不会有影响,因为一定满足if(finnishing) ,会直接跳出循环
                    finishing = advance = true;
                    //稳如老狗 ,重新检查一遍全部桶
                    //注意:这里不会受到当前线程的bound影响,执行代码顺序就是
                    //1.  while (advance)  if (--i >= bound || finishing) ,借助这里完成i递减,然后便跳出循环 (妙,膜拜)
                    //2.  下方的if ((fh = f.hash) == MOVED)   advance = true;
                    //3.  1、2循环,至 i<0, 进行本if (i < 0) {if (finishing) } ,至此结束。这里也只是对i<0起作用,跟上面说"另两个if无意义"并不冲突
                    i = n;
                }
                // 若当前桶为空,直接放一个fwd占位,表示已转移
            } else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //已经是fwd了,说明已经被处理过了,继续推进
            //拓:有文章说这里表示有别的线程进行过了,但笔者认为这个说法不严谨,因为不一定是别的线程,也有可能是当前线程处理的那部分结点,当然不影响理解,只是提一下
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 当前桶还没处理,加锁进行处理
                synchronized (f) {
                    // 再次确认当前 i 位置,确保没被其他线程修改
                    if (tabAt(tab, i) == f) {
                        //ln、hn,按笔者理解,其实就是lowNode 跟 highNode 缩写
                        ConcurrentHashMap.Node<K, V> ln, hn;
                        // hash>=0 说明是链表 , TreeBin hash = -2 , 剩下那个-3的结点不会在这里起效
                        if (fh >= 0) {
                            // 这里的逻辑跟HashMap的扩容一样,不了解的可以看我另一篇博文:https://blog.csdn.net/Unknownfuture/article/details/105181447
                            int runBit = fh & n;
                            //注意这里的实现
                            ConcurrentHashMap.Node<K, V> lastRun = f;
                            for (ConcurrentHashMap.Node<K, V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                //runBit跟lastRun 只会在 runBit发生变化时进行更改
                                // 这样在runBit都是一样的情况下,只需要指向lastRun就可以结束了,而不需要再连接上lastRun后面那些runBit一样的结点
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            //runBit为0,说明最后更新的runBit=0,同时lastRun也应该继续保留在当前位置
                            if (runBit == 0) {
                                //更新ln、hn,注意hn为null
                                ln = lastRun;
                                hn = null;
                            } else {
                                hn = lastRun;
                                ln = null;
                            }
                            //结束条件是 p != lastRun ,而不是p != null ,原因如上,节省了循环时间
                            for (ConcurrentHashMap.Node<K, V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash;
                                K pk = p.key;
                                V pv = p.val;
                                // 为0,留在本位置,否则应放到高位
                                // 注:循环结束后,会分为两条链表(ln、hn分别为两条链表头结点)
                                // 一条为逆序链表 (顺序指的是按runBit分类,在原数组的位置)
                                // 另一条链表顺序不定,而不是有的资料所说的顺序/逆序链表
                                // 而为逆序链表的那条,就是在循环前为null的那条,比如若hn=null, 那hn就是逆序链表
                                // 原因可以自己画一画,或者看笔者的图

                                // 此外,注意这里的实现是复制新的结点,而不是像hashMap中那样直接另指针指向原结点
                                // 笔者的理解:不修改原结点顺序,保证了在扩容期间,get操作也能正常使用且能返回正确结果
                                if ((ph & n) == 0)
                                    ln = new ConcurrentHashMap.Node<K, V>(ph, pk, pv, ln);
                                else
                                    hn = new ConcurrentHashMap.Node<K, V>(ph, pk, pv, hn);
                            }
                            //ln放到原位置,hn放到扩容后的位置 +n ,不多解释
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            //fwd占位
                            setTabAt(tab, i, fwd);
                            //标记当前桶处理完了,可以往前推进
                            advance = true;
                        }
                        // 红黑树类型,逻辑差不多,笔者也不是很懂红黑树,就不班门弄斧了
                        else if (f instanceof ConcurrentHashMap.TreeBin) {
                            ConcurrentHashMap.TreeBin<K, V> t = (ConcurrentHashMap.TreeBin<K, V>) f;
                            ConcurrentHashMap.TreeNode<K, V> lo = null, loTail = null;
                            ConcurrentHashMap.TreeNode<K, V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (ConcurrentHashMap.Node<K, V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                ConcurrentHashMap.TreeNode<K, V> p = new ConcurrentHashMap.TreeNode<K, V>
                                        (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                } else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new ConcurrentHashMap.TreeBin<K, V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new ConcurrentHashMap.TreeBin<K, V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

附图:对链表的处理

蓝色节点表示runBit为0,红色节点为1
假设某个桶链表结点如下:

在这里插入图片描述
此时 ln=null , hn = lastRun , for循环拆分链表后:

  1. ln链表 ,变为逆序
    在这里插入图片描述
  2. hn链表
    在这里插入图片描述
    此处举的是特例,刚好是顺序的。
    若2结点也是红色,那结果是 4->2->6->7->8 ,
    显然既不是顺序也不是逆序。

本文完,若有误,欢迎指出。

参考资料:
并发编程——ConcurrentHashMap#transfer() 扩容逐行分析
深入分析ConcurrentHashMap1.8的扩容实现

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值