ConcurrentHashMap扩容逻辑(详细)

分为两步,校验是否需要扩容扩容

校验需要扩容的流程:

  • 计算出当前数组长度,根据长度判断大于扩容阀值 同时 小于最大扩容长度(1<<30)走以下逻辑
  • 如果经历A模块(5个条件都不满足),则不扩容
  • 如果有线程在扩容,可以帮助扩容(B模块),则sizeCtl+1后进行扩容
  • 如果首次扩容(C模块),将扩容戳计算后赋值给sizeCtl后进行扩容

校验是否需要扩容的代码介绍:

   private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
     //这里就是使用longAdder分散热点的原理,不再多叙述
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
          //1.首先计算出当前数组长度
            s = sumCount();
        }
     //传进来check目的就是校验是否需要扩容,jdk8默认走这个校验逻辑
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
          //2.判断如果当前数组长度大于扩容阀值,并且不大于最大数组长度,走以下逻辑
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
              //扩容戳
                int rs = resizeStamp(n);
    
              //A模块:如果不能扩容直接break
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                  //B模块:如果可以帮助扩容,那就将sc+1,表示多了一个线程在扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
              //C模块:首次扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

⭐️总共分为四个部分,扩容戳,和A B C三个模块,分别来讲解:

1)扩容戳:

为什么需要扩容戳?它有什么含义:

  • 高 16 位代表当前扩容的标记,可以理解为一个纪元。
  • 低 16 位代表了扩容的线程数。

流程:

  • 计算当前数组长度非0位前面0的个数,与(1<<15)进行或运算,得到一个数进行返回
  • 这个计算方式得出一个必然现象,计算出来的扩容戳一定是16位,同时第一位也是1
//传入旧数组的长度,计算出一个扩容戳
int rs = resizeStamp(n);

//计算扩容戳的方法
 static final int resizeStamp(int n) {
   //例如 16 10000 那么int32位,前面就是27个0  那就是11011与10000 0000 0000 000进行或运算
   //得到10000 0000 0011011
   //因为16是最小的数组长度,所以最多就是27个0(因为数组长度肯定>=16)
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

2)C模块:首次扩容

流程:

  • 将sizectl赋值为 扩容戳无符号左移动16位后+2,赋值给sizectl,进入到扩容逻辑
        else if (U.compareAndSwapInt(this, SIZECTL, sc,(rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);

这里需要了解,为什么是无符号左移后再+2

根据刚才扩容戳的了解,高16位表示扩容标记,低16位表示扩容线程数,那么此时将扩容戳左移动之后,第一位符号位肯定是1,表示负数 (sizeCtl 为负数才代表扩容) 低16位此时全是0,+2就表示此时当前线程参与了此次扩容,线程数+1,但是为什么是+2不是+1呢?

原因是 sizeCtl 中 -1 这个数值已经被使用了,用来代替当前有线程准备扩容,所以如果直接 +1 是会和标志位发生冲突。只有初始化第一次记录扩容线程数的时候才需要 +2

3)A模块:

流程:

  • (sc >>> RESIZE_STAMP_SHIFT) != rs 这个条件实际上有 bug,在 JDK12 中已经换掉。
  • sc == rs + 1 表示最后一个扩容线程正在工作,也代表扩容即将结束。
  • sc == rs + MAX_RESIZERS (65535)表示当前已经达到最大扩容线程数,所以不能继续让线程加入扩容。
  • nextTable 为null 表示扩容完成,不进行扩容
  • transferIndex <= 0 表示当前可供扩容的下标已经全部分配完毕,也代表了当前线程扩容结束。
  if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
      sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
      transferIndex <= 0)
       break;

4)B模块:帮助扩容

流程:

  • 将sizeCtl+1后进行扩容,+1表示添加一个线程进行扩容
   if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);

5)扩容真正逻辑

流程(需要关注关键字段的含义):

  • 计算出当前线程扩容的步长(一个线程最小步长是16),赋值给stride
  • 如果未创建新数组,就初始化新数组,是原数组的2倍,然后赋值给nextTable,并将旧数组长度赋值给transferIndex
  • 根据transferIndex(总长度)和stride(步长)计算出此次线程需要扩容多少位数组下标,然后通过cas将transferIndex重新赋值(transferIndex-此次扩容的长度=剩余的需要扩容的长度),目的是需要其他线程看到是否需要帮助扩容,或者压根不用帮助
  • 如果当前数组下标为空,直接将ForwardingNode通过cas赋值到当前扩容操作的数组下标这里,目的是让其他线程对当前数组下标进行put操作时感知当前下标正在扩容,不允许put,如果失败,重新自旋进入判断
  • 如果当前数组下标不为空,先对当前数组下标加锁,进行copy操作(将旧数组下标的数据copy到新数组),copy如果完成后将fwd再赋值给旧数组节点,目的也是不允许其他线程put,直接对新数组进行操作
  • 此次操作成功后,继续对下一个数组下标进行操作,直到完成数据迁移后,通过cas将sizeCtl-1,表示自己完成扩容。

这里乱七八糟的字段有点多,含义罗列一下:

  • stride 步长,当前线程扩容涉及的数组长度

  • transferIndex 需要扩容的数组长度(旧数组长度),动态变化的,在某个线程计算出扩容长度后,它会动态的变小,其他线程也会根据此字段判断是否需要继续进行扩容

  • fwd 占位节点,如果线程A读取fwd所在数组下标的数据,线程B再对此下标进行copy或者扩容操作,线程A会暂停或者自旋重试获取数据

  • i 当前需要扩容的数组下标,会动态变小(因为扩容是数组下标从后往前,所以i最大应该是旧数组总长度)

  • advance 是否需要重新计算扩容的数组下标

  • finishing 是否已完成全部的数据迁移

  • bound 最后需要扩容的数组下标(所以扩容的总范围是nextBound~i )

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
      //计算步长
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;
      //如果首次进行扩容,新数组肯定是空,进行初始化
        if (nextTab == null) {          
            try {
                @SuppressWarnings("unchecked")
             	 //创建一个2倍大小的数组,赋值给nextTable
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {    
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
          //同时transferIndex等于旧数组长度
            transferIndex = n;
        }
        int nextn = nextTab.length;
      //创建一个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;
          //1.这里就是计算好当前扩容的总长度,必须先计算好长度才能进行扩容
            while (advance) {
                int nextIndex, nextBound;
         
                if (--i >= bound || finishing)
                    advance = false;
         
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
         //这里比较关键,将transferIndex重新进行赋值,旧数组总长度-此次扩容的长度=剩余需要扩容的数组长度
                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置空
                    nextTable = null;
                  //将新数组赋值给table
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; 
                }
            }
          //2.将当前操作的数组下标赋值
            else if ((f = tabAt(tab, i)) == null)
                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) {
                            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;
                        }
                    }
                }
            }
        }
    }

⭐️这里,copy的逻辑,需要单独拎出来讲讲:

流程:

  • 首先对当前数组下标加锁,进行双重校验,旧数组下标的元素和hash未变动,正式开始copy操作
  • 先遍历所有节点,通过runBitlastRun找到最后相同的几个节点,进行标记(目的是减少new 对象的操作和赋值,直接将这几个元素copy到另外一个数组上去)
  • 进行标记后,对链表头部元素到lastRun节点进行遍历,每个节点的hash值与数组长度进行与操作,如果结果为0存储到低位链表,如果结果为1存储到高位链表
  • 直到遍历完毕后,低位链表的数组下标原封不变,到新数组,高位链表copy的数组下标为 旧数组下标+数组长度到新数组
  • 最后,将旧数组下标的第一个元素改为fwd值,让其他线程感知到,不再put操作
 								synchronized (f) {
                  //双端判断,首先确认拿到的到底是不是这个数组下标对象
                  if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                      //其次判断,当前数组下标hash值是不是大于0,也就是它必须是非扩容状态,才能进行扩容
                        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;
                        }                      
                    }
                }
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 JDK 1.8 中,`ConcurrentHashMap` 中的扩容处理逻辑与之前的版本有所不同。在 JDK 1.8 中,`ConcurrentHashMap` 使用了一种称为“分段锁”的技术,即将整个哈希表分成多个段,每个段都有一个独立的锁,这样不同的线程可以同时访问不同的段,从而提高了并发性能。 在扩容时,`ConcurrentHashMap` 会先将整个哈希表分成多个段,然后对每个段进行扩容。具体的处理逻辑如下: 1. 首先,`ConcurrentHashMap` 会计算新哈希表的大小,然后创建一个新的哈希表数组。 2. 然后,`ConcurrentHashMap` 会对每个段进行扩容。对于每个段,`ConcurrentHashMap` 会获取该段的锁,以确保在扩容过程中不会有其他线程修改该段。 3. 在获取到锁之后,`ConcurrentHashMap` 会将该段中的所有键值对复制到新的哈希表中。在复制过程中,`ConcurrentHashMap` 会使用一种称为“链表分裂”的技术,即将每个桶中的链表分成两个链表,一个是原链表中哈希值不变的键值对构成的链表,另一个是哈希值变化的键值对构成的链表。 4. 复制完成后,`ConcurrentHashMap` 会将新的哈希表赋值给该段的哈希表属性,并释放该段的锁。 5. 最后,`ConcurrentHashMap` 会对整个哈希表进行扩容完成的检查,以确保在扩容过程中没有其他线程修改了哈希表。 需要注意的是,在 JDK 1.8 中,`ConcurrentHashMap` 的扩容操作是分段进行的,因此扩容的过程不会对整个哈希表进行加锁,从而减小了锁的争用,提高了并发性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值