ConcurrentHashMap多线程扩容原理

总结:

private transient volatile int sizeCtl;
多线程之间,以volatile的方式读取sizeCtl属性,来判断ConcurrentHashMap当前所处的状态。
通过cas设置sizeCtl属性,告知其他线程ConcurrentHashMap的状态变更。

不同状态,sizeCtl所代表的含义也有所不同:
未初始化:
    sizeCtl = 0:表示没有指定初始容量
    sizeCtl > 0:表示初始容量
初始化中:
    sizeCtl=-1,标记作用,告知其他线程,正在初始化
正常状态:
    sizeCtl = 0.75n ,扩容阈值
扩容中:
    sizeCtl < 0 : 表示有其他线程正在执行扩容
    sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此时只有一个线程在执行扩容
    sizeCtl = -N 表示,这个高16位表示当前扩容的标志,每次扩容都会生成一个不一样的标志,低16位表示参与扩容的线程数量

 

讲扩容 transfer(tab, null);
先讲一下扩容的总体思路:
扩容的时候 会算出一个步长 根据cpu的核心数量来算,最小为16 现在为了方便 假设 数组数量为4 步长为2 那么就会通过计算算出A线程的步长是2开始位置是下标是3 2 默认是从数组从右像左扩容的 B线程来了算出的位置就是1 0 然后把对应位置转移过去,并且会把对应位置更新为一个forwardingNode 他的hash值为1 这也是put操作中判断正在扩容并且帮助扩容的重要标志。transferIndex=4
————————————————

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //这里就是算步长,根据cpu核心数 最小为16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//新数组
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;//这个变量代表了当前线程是否需要继续扩容
             //假设当然map容量为4 步长为1 只要A线程扩容每次扩容一个 那么下一次肯定是要继续扩容的
             //但是也有可能有多个线程同时扩容 我扩容完了 其他线程把整个也扩容完了就不需要前进,继续扩容了后面会修改为false    


            //判断当前线程的工作是否完成 跟前面一样 假设只要A线程再扩容 那么我肯定还需要再继续工作,
            //但是如果其他线程正在扩容其他的 我就完成了 就修改完true
        boolean finishing = false; // to ensure sweep before committing nextTab    

        //这里的I和bound带变了 每次扩容的边界
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                //如果i减一以后还是>=bound说明还在步长内不需要继续前进扩容
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                //下一个边界都小于=0了说明也不继续前进了
                    i = -1;
                    advance = false;
                }
                //第一次回进入到这里 假设map容量为4 步长为2
                //TRANSFERINDEX再前面看到是等于n=4的
                //nextIndex=TRANSFERINDEX=4
                //nextBound=4-2=2;
                //i=nextIndex=1=3
                //同时cas修改 TRANSFERINDEX为nextBound
                //现在明白了把 就是这样算的  
                //假设其他线程也修改 他们获得的TRANSFERINDEX是2 注意看上面的else if 这时候
                //来了一个线程B获取到的TRANSFERINDEX就是2 算出的i和bound就是1 和0  
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //判断sizectl 是否等于最开始那个初始值 等于初始值代表所有线程扩容完毕
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            //如果说当前位置为空 说明不需要转移 之间设置fwd 防止其他线程这时候插入新值
           //扩容的时候肯定不能插入嘛
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
                //跟null一样的道理
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
            //这里就是锁住头节点不让转移的时候插入
            //下面开始真正的转移 转移无非就是再原位置和原位置+旧容量  
            //找到所有再同一个位置的 改组成链表的就是链表 树的就是树然后统一转移  类似蜘蛛纸牌
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new 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 TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }


扩容流程:
1、根据操作系统的 CPU 核数和集合 length 计算每个核一轮处理桶的个数,最小是16
2、修改 transferIndex 标志位,每个线程领取完任务就减去多少,
比如初始大小是transferIndex = table.length = 64,每个线程领取的桶个数是16,
第一个线程领取完任务后transferIndex = 48,也就是说第二个线程这时进来是从第 48 个桶开始处理,再减去16,依次类推,这就是多线程协作处理的原理
3、领取完任务之后就开始处理,如果桶为空就设置为 ForwardingNode ,
如果不为空就加锁拷贝,只有这里用到了 synchronized 关键字来加锁,为了防止拷贝的过程有其他线程在put元素进来。
拷贝完成之后也设置为 ForwardingNode节点。
4、如果某个线程分配的桶处理完了之后,再去申请,发现 transferIndex = 0,
这个时候就说明所有的桶都领取完了,但是别的线程领取任务之后有没有处理完并不知道,
该线程会将 sizeCtl 的值减1,然后判断是不是所有线程都退出了,如果还有线程在处理,就退出,
直到最后一个线程处理完,发现 sizeCtl = rs<< RESIZE_STAMP_SHIFT 也就是标识符左移 16 位,
才会将旧数组干掉,用新数组覆盖,并且会重新设置 sizeCtl 为新数组的扩容点。

以上过程总的来说分成两个部分:
1、分配任务:这部分其实很简单,就是把一个大的数组给切分,切分多个小份,然后每个线程处理其中每一小份,
当然可能就只有1个或者几个线程在扩容,那就一轮一轮的处理,一轮处理一份。
2、处理任务:复制部分主要有两点,第一点就是加锁,第二点就是处理完之后置为ForwardingNode来占位标识这个位置被迁移过了。

ForwardingNode用于占位。当别的线程发现这个槽位中是 fwd 类型的节点,则跳过这个节点。
————————————————
版权声明:本文为优快云博主「Hanyinh」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/weixin_43871678/article/details/115966911

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值