Java并发编程(六)ConcurrentHashMap的扩容

154 阅读7分钟

三种触发方式

  • 当前容量超过阈值
  • 当链表中元素个数超过默认设定(8个),当数组的大小还未超过64的时候,此时进行数组的扩容
  • 当发现其他线程扩容时,协助扩容

1. tryPreSize方法-初始化数组

// 扩容前操作,putAll,链表转红黑树  插入map的长度(putAll)
private final void tryPresize(int size) {
    // 这个判断是给putAll留的,要计算当前数组的长度(初始化)
    // 如果size大于最大长度 / 2,直接将数组长度设置为最大值。
    // tableSizeFor,将长度设置的2的n次幂
    // c是初始化数组长度
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
    // sc是给sizeCtl赋值
    // -1:正在初始化数组,小于-1:正在扩容,0:代表还没初始化数组,大于0:可能初始化了(代表阈值),也可能没初始化(初始化的长度)
    int sc;
    while ((sc = sizeCtl) >= 0) {
        // 代表没有正在执行初始化,也没有正在执行扩容。、
        // tab:数组,n:数组长度
        Node<K,V>[] tab = table; int n;
        // 判断数组是不是还没初始化呢
        if (tab == null || (n = tab.length) == 0) {
            // 初始化数组,和initTable一样的东西
            // 在sc和c之间选择最大值,作为数组的初始化长度
            n = (sc > c) ? sc : c;
            // 要初始化,就直接把sizeCtl设置为-1,代表我要初始化数组
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // DCL!
                    if (table == tab) {
                        // 创建数组
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        // 初始化数组赋值给成员变量
                        table = nt;
                        // sc先设置成阈值
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 将sc赋值给sizeCtl
                    sizeCtl = sc;
                }
            }
        }
        // 要么是c没有超过阈值,要么是超过最大值,啥事不做~~~
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        // 省略部分代码。
    }
}

2. tryPreSize方法-扩容标识戳

// 扩容前操作
private final void tryPresize(int size) {
    while ((sc = sizeCtl) >= 0) {
        // 省略部分初始化代码
        Node<K,V>[] tab = table; int n;
        if (tab == null || (n = tab.length) == 0) {
        // 扩容前操作!
        else if (tab == table) {
            // 计算扩容标识戳(基于老数组长度计算扩容标识戳,因为ConcurrentHashMap允许多线程迁移数据。)
            int rs = resizeStamp(n);
            // 这里是一个BUG,当前sc在while循环中,除了初始化没有额外赋值的前提下,这个sc < 0 永远进不来。
            // 虽然是BUG,但是清楚sc < 0 代表正在扩容
            if (sc < 0) {
                Node<K,V>[] nt;    31 ~ 16   15 ~ 0
                // 这里是第二个BUG
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs ||   // 判断协助扩容线程的标识戳是否一致
                    sc == rs << RESIZE_STAMP_SHIFT + 1 ||    // BUG之一,在判断扩容操作是否已经到了最后的检查阶段
                    sc == rs << RESIZE_STAMP_SHIFT + MAX_RESIZERS ||   // BUG之一,判断扩容线程是否已经达到最大值
                    (nt = nextTable) == null ||  // 新数组为null,说明也已经扩容完毕,扩容完毕后,才会把nextTable置位null
                    transferIndex <= 0) // transferIndex为线程领取任务的最大节点,如果为0,代表所有老数据迁移任务都没领干净了
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 还没有执行扩容,当前线程可能是第一个进来执行扩容的线程
            // 基于CAS的方式,将sizeCtl从原值改为 扩容标识戳左移16位
            // 10000000 00011010 00000000 00000010  一定是< -1的负数,可以代表当前ConcurrentHashMap正在扩容
            // 为什么是低位+2,代表1个线程扩容。 低位为5,就代表4个线程正在并发扩容
            // 扩容分为2部:创建新数组,迁移数据。
            // 当最后一个线程迁移完毕数据后,对低位-1.最终结果低位还是1,需要对整个老数组再次检查,数据是否迁移干净
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                // 开始扩容操作,传入老数组~~
                transfer(tab, null);
        }
    }
}


static final int resizeStamp(int n) {
    // 32~64
    // 00000000 00000000 00000000 00011010
    // 计算n在二进制表示时,前面有多少个0
    // 00000000 00000000 10000000 00000000
    // 00000000 00000000 10000000 00011010
    // 前面的操作是基于数组长度等到一个标识,方便其他线程参与扩容
    // 后面的值是为了保证当前扩容戳左移16位之后,一定是一个负数
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}

三、transfer方法-构建新数组

transfer方法:

  • 计算步长
  • 初始化新数组
  • 线程领取迁移数据任务
  • 判断迁移是否完成,并判断当前线程是否是最后一个完成的
  • 查看当前位置数据是否为null
  • 查看当前位置数据是否为fwd
  • 链表迁移数据-lastRun机制
  • 红黑树迁移-迁移完数据长度小于等于6,转回链表
// 扩容操作,以第一个进来执行扩容的线程为例。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 创建新数组流程!
    // n:老数组长度32,   stride:扩容的步长16
    int n = tab.length, stride;
    // NCPU:4
    // 00000000 00000000 00000000 00000000
    // 00000000 00000000 00000100 00000000  - 1024 512 256 128 / 4 = 32
    // 如果每个线程迁移的长度基于CPU计算,大于16,就采用计算的值,如果小于16,就用16
    // 每个线程每次最小迁移16长度数据
    // stride = 1 < 16
    // 这个操作就是为了充分发挥CPU性能,因为迁移数据是CPU密集型操作,尽量让并发扩容线程数量不要太大,从而造成CPU的性能都消耗在了切换上,造成扩容效率降低
    // 如果要做优化的,推荐将扩容线程数设置为和CPU内核数+1一致。
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) 
        stride = MIN_TRANSFER_STRIDE; 
​
    // 如果新数组没有初始化
    if (nextTab == null) {   
        try {
            // 初始化数组
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            // 新数组赋值给nextTab
            nextTab = nt;
        } catch (Throwable ex) {   
            // 要么OOM,要么数组长度达到最大值。
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 将nextTable成员变量赋值
        nextTable = nextTab;
        // transferIndex设置为老数组长度
        transferIndex = n;
    }
  
}
​
// n:老数组长度
// stride:步长
// nextTale,nextTab:新数组
// transferIndex:线程领取任务时的核心属性

四、transfer方法-迁移数据

第一步,线程领取迁移数据的任务

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 省略部分代码
    // n:老数组长度   32
    // stride:步长   16
    // nextTale,nextTab:新数组
    // nextn:新数组长度  64 
    // transferIndex:线程领取任务时的核心属性 32
    // 先看领取任务的过程!!!
    // 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 领任务的核心标识
    boolean advance = true;
    // 扩容结束了咩?
    boolean finishing = false;
    // 扩容的for循环
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 领取任务的while循环
        while (advance) {
            int nextIndex, nextBound;
            // 第一个判断是为了迁移下一个索引数据(暂时不管)
            if (--i >= bound || finishing)
                advance = false;
            // 说明没有任务可以领取了(暂时不管)
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // transferIndex:16
            // stride:16,nextIndex:32,nextBound:16
            // bound:16,i:31
            // 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }

第二步:判断是否结束,以及线程退出扩容,并且为空时,设置fwd,并且hash为moved直接移动到下个位置

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 省略部分代码
    // n:老数组长度   32
    // stride:步长   16
    // nextTale,nextTab:新数组
    // nextn:新数组长度  64 
    // transferIndex:线程领取任务时的核心属性 32
    // 先看领取任务的过程!!!
    // 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 领任务的核心标识
    boolean advance = true;
    // 扩容结束了咩?
    boolean finishing = false;
    // 扩容的for循环
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 领取任务的while循环
        while (advance) {
            int nextIndex, nextBound;
            // 第一个判断是为了迁移下一个索引数据(暂时不管)
            if (--i >= bound || finishing)
                advance = false;
            // 说明没有任务可以领取了(暂时不管)
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // transferIndex:16
            // stride:16,nextIndex:32,nextBound:16
            // bound:16,i:31
            // 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        // 迁移最后一段的线程干完活了,或者其他线程没有任务可以领取了。
        if (i < 0) {
            int sc;
            // 判断结束了没,第一次肯定进不来
            if (finishing) {
                // 结束扩容,将nextTabl设置为null
                nextTable = null;
                // 将迁移完数据的新数组,指向指向的老数组
                table = nextTab;
                // 将sizeCtl复制为下次扩容的阈值
                sizeCtl = (n << 1) - (n >>> 1);
                // 结束
                return;
            }
            // 到这,说明当前线程没有任务可以领取了
            // 基于CAS的方式,将低位-1,代表当前线程退出扩容操作(如果是最后一个,还有一个额外的活)
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 判断我是否是最后一个完成迁移数据的线程,如果不是,直接return结束
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 如果到这,说明我是最后一个结束迁移数据的线程。
                // finishing结束表示和advance领取任务的标识全部设置为true
                finishing = advance = true;
                // i设置为老数组长度,从头到位再检查一次整个老数组。
                i = n; 
            }
            /*
            额外分析:当前线程完成领取的迁移任务后,再次进入while循环,
            查看是否有任务可以领取,如果transferIndex变为0了,代表我没有任务可以领取,
            将i设置为-1没有任务可以领取,退出当前扩容操作:
            1、基于CAS将sizeCtl - 1代表我退出扩容操作
            2、-1成功后,还要判断,我是不是最后一个退出扩容的线程(sc - 2值是否是 扩容标识戳 << 16)如果不是,直接return结束
            3、如果是最后一个结束迁移的线程,将i复制为老数组长度,重新从末位到头部再次检查一圈
            */
        }
        else if ((f = tabAt(tab, i)) == null)
            // 如果发现迁移为主的数据为null,设置放置一个fwd,代表当前位置迁移完成
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            // 是在检查时的逻辑
            advance = true; 

五、transfer方法-lastRun机制

就是迁移链表到新数组时的操作

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 省略部分代码
    // n:老数组长度   32
    // stride:步长   16
    // nextTale,nextTab:新数组
    // nextn:新数组长度  64 
    // transferIndex:线程领取任务时的核心属性 32
    // 先看领取任务的过程!!!
    // 声明fwd节点,在老数组迁移数据完成后,将fwd赋值上去
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 领任务的核心标识
    boolean advance = true;
    // 扩容结束了咩?
    boolean finishing = false;
    // 扩容的for循环
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 领取任务的while循环
        while (advance) {
            int nextIndex, nextBound;
            // 第一个判断是为了迁移下一个索引数据(暂时不管)
            if (--i >= bound || finishing)
                advance = false;
            // 说明没有任务可以领取了(暂时不管)
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // transferIndex:16
            // stride:16,nextIndex:32,nextBound:16
            // bound:16,i:31
            // 开始领取任务,如果CAS成功,代表当前线程领取了32~16这个范围数据的迁移
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        // 迁移最后一段的线程干完活了,或者其他线程没有任务可以领取了。
        if (i < 0) {
            int sc;
            // 判断结束了没,第一次肯定进不来
            if (finishing) {
                // 结束扩容,将nextTabl设置为null
                nextTable = null;
                // 将迁移完数据的新数组,指向指向的老数组
                table = nextTab;
                // 将sizeCtl复制为下次扩容的阈值
                sizeCtl = (n << 1) - (n >>> 1);
                // 结束
                return;
            }
            // 到这,说明当前线程没有任务可以领取了
            // 基于CAS的方式,将低位-1,代表当前线程退出扩容操作(如果是最后一个,还有一个额外的活)
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 判断我是否是最后一个完成迁移数据的线程,如果不是,直接return结束
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 如果到这,说明我是最后一个结束迁移数据的线程。
                // finishing结束表示和advance领取任务的标识全部设置为true
                finishing = advance = true;
                // i设置为老数组长度,从头到位再检查一次整个老数组。
                i = n; 
            }
            /*
            额外分析:当前线程完成领取的迁移任务后,再次进入while循环,
            查看是否有任务可以领取
            如果transferIndex变为0了,代表我没有任务可以领取,
            将i设置为-1没有任务可以领取,退出当前扩容操作:
            1、基于CAS将sizeCtl - 1代表我退出扩容操作
            2、-1成功后,还要判断,我是不是最后一个退出扩容的线程(sc - 2值是否是 扩容标识戳 << 16)如果不是,直接return结束
            3、如果是最后一个结束迁移的线程,将i复制为老数组长度,重新从末位到头部再次检查一圈
            */
        }
        else if ((f = tabAt(tab, i)) == null)
            // 如果发现迁移为主的数据为null,设置放置一个fwd,代表当前位置迁移完成
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            // 是在检查时的逻辑
            advance = true; 
        else {
            // 迁移数据,加锁!
            synchronized (f) {
                // 拿到当前位置数据
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 说明当前节点状态正常,不是迁移,不是红黑树,不是预留
                    if (fh >= 0) {
                        // fh与老数组进行&运算,得到runBit
                        // 00001111
                        // 00010000
                        // 这个计算的结果,会决定当前数据在迁移时,是放到新数组的i位置还有新数组的 i + n位置
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        // lastRun机制
                        // 提前循环一次链表,将节点赋值到对应的高低位Node./
                        // 如果链表最后面的值没有变化,那就不动指针,直接复制。
                        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位置,不再继续往下循环
                        // 这样可以不用每个节点都new,避免GC和OOM问题。
                        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);
                        // 将当前迁移完的桶位置,设置上fwd,代表数据迁移完毕
                        setTabAt(tab, i, fwd);
                        // advance,代表执行下次循环,i--。
                        advance = true;
                    }
                    // 省略红黑树迁移!
                }
            }
        }
    }
}

六、helpTransfer方法-协助扩容

// 协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 老数组不为null,当前节点是fwd,新数组不为null
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 创建自己的扩容标识戳
        int rs = resizeStamp(tab.length);
        // 判断之前赋值的内容是否有变化,并且sizeCtl是否小于0
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || 
                sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || 
                transferIndex <= 0)
                // 有一个满足,就说明不需要协助扩容了
                break;
            // CAS,将sizeCtl + 1,代表来协助扩容了
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}