并发编程源码解析(六)ConcurrentHashMap源码解析之三并发扩容

一、前瞻

并发编程源码解析(四)ConcurrentHashMap源码解析之一基础概念介绍以及散列算法讲解-CSDN博客

并发编程源码解析(五)ConcurrentHashMap源码解析之二读与写源码分析-CSDN博客

二、介绍 

        首先,我们先来复习一下 sizeCtl 的状态,

-1     正常初始化

< -1  是正在扩容

0      没有初始化

> 0    数组扩容阈值/ 没有初始化是代表初始化长度

        接着我们来介绍一下关于并发扩容的基本概念, ConcurrentHashMap 的第一个线程进入线程之后会将 sizeCtl 设置为 -2,然后将新的数组创建好,并将扩容任务拆分成若干个子任务,当其他线程进行 put 时,如果检测到了正在扩容就会将 sizeCtl +1 (会变成-3) 以表示新的线程参与并发扩容;当任务结束时线程退出扩容就会将 sizeCtl -1, 最后一个线程退出时,则会检查一遍数组是否全部迁移完毕,则会将 sizeCtl 设置为下次扩容的阈值并退出扩容。

        ConcurrentHashMap 的扩容原理是非常重要的一部分内容,因为它扩容并不是由单线程执行的而是多线程并发执行的。

这过程一共会在以下几个场景体现出来,本文主要的内容是有tryPresize 作为引子来讲解真正的迁移方法 transfer

  1. 转换成红黑树的 treeifyBin 方法由 tryPresize 方法触发。
  2. 另外一个就是 addCount 方法。

三、源码解析

3.1 tryPresize 的源码解析

3.1.1 tryPresize 的触发

tryPresize 会在当长度小于 64 时若转换成红黑树的情况,就会触发扩容方法,以减少红黑树的形成。

 private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //当长度小于64时触发扩容方法
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                // 省略部分代码
            }
        }
    }

3.1.2 tryPresize  源码解析

     private final void tryPresize(int size) {
        //如果 size 大于最大容量的一半,则赋值为最大容量
        //tableSizeFor 的主要功能是让不等于二的n次幂的变到离它最近的比它大的二的n次幂
        //如:传入9 则 变成 16
        //MAXIMUM_CAPACITY 的 大小为 1 <<< 30 是因为 sizeCtl 的前两位是用来存储它的状态的
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
                tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        // 如果sizeCtl大于等于0则没有正在扩容,则触发扩容流程
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //tab为空 进入初始化流程;初始化流程已经在上期介绍过了这里不赘述
            if (tab == null || (n = tab.length) == 0) {
               //省略部分代码
            }
            // c 如果小于现在的 sizeCtl 证明已经有其他线程扩容完毕退出循环
            // 现有数组长度已经达到最大值也退出循环
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            // 最后再检查一遍table 有没有被其他人扩容过
            else if (tab == table) {
                // Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1))
                // 计算n前面有多少个的0,并保证他的第十六位是1;这里的目的十位确保他左移十六位后是负数
                int rs = resizeStamp(n);
                //这是一个bug,这里的sc不可能小于0,因为这里没有任何操作对sc赋值
                if (sc < 0) {
                    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);
                }
                //进行扩容,第一个进入的线程+2,进入扩容流程
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
            }
        }
    }

3.2 transfer 方法

3.2.1 transfer方法-计算每个线程迁移的长度

// 开始扩容   tab=oldTable
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // n = 数组长度
    // stride = 每个线程一次性迁移多少数据到新数组
    int n = tab.length, stride;
    // 基于CPU的内核数量来计算,每个线程一次性迁移多少长度的数据最合理
    // NCPU = 4
    // 举个栗子:数组长度为1024 - 512 - 256 - 128 / 4 = 32
    // MIN_TRANSFER_STRIDE = 16,为每个线程迁移数据的最小长度
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; 
    // 根据CPU计算每个线程一次迁移多长的数据到新数组,如果结果大于16,使用计算结果。 如果结果小于16,就使用最小长度16
}

3.2.2 transfer方法-构建新数组并查看标识属性

// 以32长度数组扩容到64位例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 省略部分代码
    // n = 老数组长度 32;stride = 步长 16
    // 第一个进来扩容的线程需要把新数组构建出来
    if (nextTab == null) {
        try {
            // 将原数组长度左移一位(x2),构建新数组长度
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {   
            // 到这说明已经达到数组长度的最大取值范围
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 将成员变量的新数组赋值
        nextTable = nextTab;
        // 迁移数据时,用到的标识,默认值为老数组长度
        transferIndex = n;   // 32
    }
    // 新数组长度
    int nextn = nextTab.length;  // 64
    // 在老数组迁移完数据后,做的标识, get识别到就会直接来新数组查询
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 迁移数据时,需要用到的标识
    boolean advance = true;
    boolean finishing = false; 
    // 省略部分代码
}

3.2.3 transfer方法-线程领取迁移任务

// 以32长度扩容到64位为例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // stride:16 n:32
    int n = tab.length, stride;
    if (nextTab == null) { 
        // 省略部分代码…………
        // nextTable:新数组
        nextTable = nextTab;
        // transferIndex:0
        transferIndex = n;
    }
    // nextn:64
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // advance:true,代表当前线程需要接收任务,然后再执行迁移,如果为false,代表已经接收完任务
    boolean advance = true;
    // finishing:false,是否迁移结束!
    boolean finishing = false; 
    // 循环……
    // i = 15     代表当前线程迁移数据的索引值!!
    // bound = 0
    for (int i = 0, bound = 0;;) {
        // f = null
        // fh = 0
        Node<K,V> f; int fh;
        // 当前线程要接收任务
        while (advance) {
            int nextIndex, nextBound;
            // 对i进行--,并且判断当前任务是否处理完毕!
            if (--i >= bound || finishing)
                advance = false;
            // 判断transferIndex是否小于等于0,代表没有任务可领取就结束任务。
            // 在线程领取任务会,会对transferIndex进行修改,修改为transferIndex - stride
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // 当前线程尝试领取任务
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                // 设置advance设置为false,代表当前线程领取到任务了。
                advance = false;
            }
        }
        // 开始迁移数据,并且在迁移完毕后,会将advance设置为true
  
    }
}

3.2.4 transfer方法-迁移结束操作

// 以32长度扩容到64位为例子
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    for (int i = 0, bound = 0;;) {
        while (advance) {
        // 判断扩容是否已经结束!
        // i < 0:当前线程没有接收到任务!
        // i >= n: 迁移的索引位置,不可能大于数组的长度,不会成立
        // i + n >= nextn:因为i最大值就是数组索引的最大值,不会成立
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // finishing为true,代表扩容结束
            //重新计算扩容阈值; 如:16 - 4 = 12
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // 当前线程没有接收到任务,让当前线程结束扩容操作。
            // 采用CAS的方式,将sizeCtl - 1,代表当前并发扩容的线程数 - 1
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // sizeCtl的高16位是基于数组长度计算的扩容戳,低16位是当前正在扩容的线程个数
                // 代表当前线程并不是最后一个退出扩容的线程,直接结束当前线程扩容
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 如果是最后一个退出扩容的线程,将finishing和advance设置为true
                finishing = advance = true;
                // 将i设置为老数组长度,让最后一个线程再从尾到头再次检查一下,是否数据全部迁移完毕。
                i = n; 
            }
        }
        // 开始迁移数据,并且在迁移完毕后,会将advance设置为true 
    }
}

3.2.5  transfer方法-迁移数据(链表)

     private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        // 省略部分代码
        for (int i = 0, bound = 0;;) {
            // 省略部分代码
            //  如果当前的桶没有数据,则无需迁移,直接将当前桶位置设置为fwd
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // 如果已经迁移则跳过当前位置
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 当前桶位置有数据,先锁住当前桶位置。
                synchronized (f) {
                    // DCL 判断之前取出的数据是否为当前的数据。
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        // hash大于0,不是红黑树,使用链表方式迁移数据
                        if (fh >= 0) {
                            // lastRun机制 这种运算结果只有两种,要么是0,要么是n;
                            // 目的是为了减少 hash 迁移的复杂度
                            // 如: 8 -> 16, 1 位置要么保持在1 要么迁移到5
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            // 循环的目的就是为了得到链表下经过hash & n结算,分别储存到高低位
                            // 在迁移数据时,值需要迁移到lastRun即可,剩下的指针不需要变换。
                            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;
                            }
                            // 循环到lastRun指向的数据即可,后续不需要再遍历
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                // 获取当前Node的hash值,key值,value值。
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                // 如果hash&n为0,挂到lowNode上
                                // 如果hash&n为n,挂到highNode上
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // 采用CAS的方式,将ln挂到新数组的原位置
                            setTabAt(nextTab, i, ln);
                            // 采用CAS的方式,将hn挂到新数组的原位置 + 老数组长度
                            setTabAt(nextTab, i + n, hn);
                            // 采用CAS的方式,将当前桶位置设置为fwd
                            setTabAt(tab, i, fwd);
                            // advance设置为true,保证可以进入到while循环,对i进行--操作
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            //红黑树迁移省略
                        }
                    }
                }
            }
        }
    }

 

3.3 helpTransfer方法-协助扩容

 final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        // 判断新老数组都不为空
        if (tab != null && (f instanceof ForwardingNode) &&
                (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            // 基于老数组长度计算扩容戳
            int rs = resizeStamp(tab.length);
            // fwd中的新数组,和当前正在扩容的新数组是否相等。相等:可以协助扩容。不相等:要么扩容结束,要么开启了新的扩容
            while (nextTab == nextTable
                    // 老数组是否改变了。 相等:可以协助扩容。不相等:扩容结束了
                    && table == tab
                    // 如果正在扩容,sizeCtl肯定为负数,并且给sc赋值
                    && (sc = sizeCtl) < 0) {
                // 将sc右移16位,判断是否与扩容戳一致。 如果不一致,说明扩容长度不一样,退出协助扩容
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs
                        // 这两个是bug 要右移 16位判断才成立
                        || sc == rs + 1
                        || sc == rs + MAX_RESIZERS
                        // transferIndex是从高索引位置到低索引位置领取数据的一个核心属性(transfer 当中计算)
                        // 如果满足 小于等于0,说明任务被领光了。
                        || transferIndex <= 0)
                    break;
                // CAS 修改线程进入协助扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

四、作者的话

这玩意解读起来也太累了吧,人要疯掉!!!!!

 

  • 43
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值