ConCurrentHashMap并发环境下,如何扩容?

在之前的博文《 ConCurrentHashMap 的源码分析》,系统分析了源码。

文章特别的长,这篇文章,只摘录扩容这一个点,详细分析。

sizeCtl 参数

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

不严谨的来说:如果 sizeCtl = -1,说明是在初始化。

如果 -(n+1) 说明有 n 个线程在扩容。

如果严谨的来说,sizeCtl 的低16位如果是 n,n = -1 表示数组在初始化。

n = 10,代表有9个线程正在扩容,同理,n = 2,代表只有一个线程在扩容。

那高16位是干啥的,高16位记录了旧数组的长度信息。

ConcurrentHashMap 初始化时,会设置sizeCtl


//第一条扩容线程设置的某个特定基数
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)

//后续线程加入扩容大军时每次加 1
U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)

//线程扩容完毕退出扩容操作时每次减 1
U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)

在上篇文章《 addCount 触发扩容》 详细讲了这个参数,这里不再展开。

transfer 扩容


  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; // 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;
      boolean finishing = false; // to ensure sweep before committing nextTab
      for (int i = 0, bound = 0;;) {
          Node<K,V> f; int fh;
          while (advance) {
              int nextIndex, nextBound;
              if (--i >= bound || finishing)
                  advance = false;
              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;
              }
          }
          if (i < 0 || i >= n || i + n >= nextn) {
              int sc;
              if (finishing) {
                  nextTable = null;
                  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; // recheck before commit
              }
          }
          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) {
                  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;
                      }
                  }
              }
          }
      }
  }

代码很长,也很不好理解。先不解析源码,先要明白以下几点。

原数组长度 n,新数组长度为 2n。扩容时,每次转移固定的节点数(stride)。

在这里插入图片描述

比如上图,当前线程,转移红色的节点。

若出现并发,并发线程转移灰色的节点。

即从末尾开始转移,每次固定的步长(长度 stride);

  1. 当本次transfer()结束后,调用方会决定是否再次调用transfer()
  2. 并发扩容时,每个线程,转移各自的节点(长度 stride)。
  3. 多个 stride,从旧数组末尾开始算,不重不漏。
  4. CAS操作 transferIndex,用于并发状态下,控制不同线程的。
  5. 具体节点从旧数组,转移到新数组,代码与HashMap相似。

好,看过这几条之后,开始一点一点解析源码。

   if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
       stride = MIN_TRANSFER_STRIDE; // subdivide range

   private static final int MIN_TRANSFER_STRIDE = 16;

这个是算步长的,和CPU数有关。步长最小值是 16。

   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;
   }

在上一篇文章中,《addCount 启动扩容》,transfer(tab, null); 传入的新数组是 null。

在创建新数组时,会设置 transferIndex = n

    /**
     * The next table index (plus one) to split while resizing.
     */
    private transient volatile int transferIndex;

当出现并发扩容时,这个全局变量,是用来给各个线程分配节点的。


    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true; // 是否结束 while 循环
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) { }
      

这里 for 循环做了两件事,先是计算需要转移的节点,再是将每个节点的数据进行转移


   while (advance) {
       int nextIndex, nextBound;
       if (--i >= bound || finishing)
           ……
       else if ((nextIndex = transferIndex) <= 0) {
           ……
       }
       else if (U.compareAndSwapInt
                (this, TRANSFERINDEX, nextIndex,
                 nextBound = (nextIndex > stride ?
                              nextIndex - stride : 0))) {
           bound = nextBound;
           i = nextIndex - 1;
           advance = false;
       }
   }
   

这里的三个分支,最后一个是计算需要转移的节点。

nextBound = (nextIndex > stride ? nextIndex - stride : 0) 这行不难理解。

比如原数组长度是 64,那新数组长度是 128。transferIndex 的初始值是 64

stride 是 16,那计算出 bound = nextBound = 48, i = 63

U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound )

执行之后,transferIndex 被 CAS 操作为 48。

那本次转移的节点,就是下标 48 到下标题 63 之间的数据。

如果并发扩容,别的线程进来了,会以新的transferIndex = 48 来计算,需要转移哪些数据。

以上分析了并发扩容时,对不同线程处理节点的控制。

Doug Lea 真的是牛,不用锁,仅用 CAS 控制一个全局变量,

就很巧妙的控制了并发扩容问题。

前面说过,for 循环做了两件事,先是计算本次需要转移的节点

再是将每个节点的数据进行转移。现在讲数据迁移的问题。

接着上面的例子,计算出 i = 63


    if (i < 0 || i >= n || i + n >= nextn) { // 结束扩容的,先不用管
        ……
    }
    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) {
        ……
        setTabAt(tab, i, fwd); // 将旧数组该节点处 hash 设置为 MOVED
        advance = true;
        }
    }

f = tabAt(tab, i) 这是取出该节点,判断这个节点,有三种情况,

  1. 节点为 null (即没有数据要迁移)
  2. hash 值为 -1,会再下一次for循环中,开启while循环
  3. 其它情况 进行数据迁移,将旧数组设置一个特殊节点 fwd

假设 i = 63 这个节点是 null,属于第一种情况,

那就执行 advance = casTabAt(tab, i, null, fwd);

	ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
     }

fwd 是一个空节点,只是 hash 值是 MOVED( 即-1 )。

执行 advance = casTabAt(tab, i, null, fwd); ,这是CAS 操作,

如果失败,结束本次 for 循环,下次 for 循环还会执行这行,

如果成功,advance = true,结束本次 for 循环,下次 for 循环时,

开启 while 循环, while (advance) {}


   while (advance) {
       int nextIndex, nextBound;
       if (--i >= bound || finishing)
           advance = false;
       else if ((nextIndex = transferIndex) <= 0) {
           ……
       }
       else if ( ) {
          ……
       }
   }
   

进入 while 循环后,执行 --i >= bound 这行,i 从 63 变为 62。相当于处理下个节点。

也就是说,在上个循环中,处理 i = 63 时,会有三种情况,现在完善下,就是

  1. 节点为 null ,设置一个 MOVED 节点,结束本次循环,在下次循环时,下标会减1。
  2. 节点的 hash 值为 MOVED,说明本节点已处理过,结束本次循环,在下次循环时,下标减1。
  3. 其它情况 进行数据迁移,即头节点加锁,迁移数据,完成迁移后,设置hash 值为 MOVED,结束本次循环,下次循环时,下标减1。

总之会将节点设置为 MOVED,并将下标减小 1 。

那什么时候,结束数据迁移呢?

还是以上面的例子,转移下标 48 到 63 的数据。

当这几个节点都转移完成了。即 i = 48 会再次进入 while 循环

bound = nextBound = 48 这两个参数,最开始计算过,是 48。

  while (advance) {
      int nextIndex, nextBound;
      if (--i >= bound || finishing)
          advance = false;
      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;
      }
  }

if (--i >= bound || finishing) 这个不满足,

else if ((nextIndex = transferIndex) <= 0) 这个也不满足,

那就进入第三个分支,重新计算下标,设置 transferIndex 这个全局变量。

在这里插入图片描述
就比如说,某线程是转移 红色的那几个节点,

转移完了,灰色的几个节点分给其它线程了,

那这个线程再分配时,可能就分配到绿色的那几个节点。

当所有节点都分配完了,那就该结束 transfer 这个方法了。


  if (i < 0 || i >= n || i + n >= nextn) {
      int sc;
      if (finishing) {
          nextTable = null;
          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; // recheck before commit
      }
  }
       

整体代码的思路是:

多个线程在扩容,当某线程完成自己的扩容任务时,退出

最后一个扩容线程,将 finishing 设置为 true。

然后将旧数组检查一遍(即再次走一遍扩容代码),

确实是所有节点都已完成迁移,设置相关参数,结束扩容。

U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1) 这行之前说过,

某线程完成分给它的迁移任务,退出时,记录扩容 线程的数量要减小1。

if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) 这行是不是有点懵。

在讲 addCount() 触发扩容时,讲了这段


   else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)){
        transfer(tab, null);
   }

触发扩容的那个线程,设置 sizeCtl 的值是 rs << RESIZE_STAMP_SHIFT) + 2

之后若有线程加入扩容,sizeCtl 的值 加 1,有线程退出扩容,这个值 减 1。

if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

它的意思是,当前扩容线程数量不是1。

如果当前扩容线程数量是 1,

那应该是 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT

当前扩容线程数量是 1 时,会设置 finishing = advance = true;

这会开启 while 循环。另外设置 i = n;

这会从旧数组最后一个节点开始,重新执行扩容代码,

检查所有的节点 hash 值是否等于 MOVED。

检查完毕,执行下面的代码


  if (finishing) {
      nextTable = null;  // 参数置空
      table = nextTab; // table 使用新数组
      sizeCtl = (n << 1) - (n >>> 1); // sizeCtl 设置为当前数组大小的 0.75 倍,做为下次扩容的阈值。
      return;
  }

头节点加锁,进行数据迁移,这个与 HashMap 的代码雷同。

这里不再分析,不清楚的可以参考《HashMap 源码解析》。

至此,整个扩容的源码流程分析完了。

小结

  • 插入元素时,键值对达到阈值,会启动扩容。
  • 链表转红黑树时,若数组长度小于64,会启动扩容。
  • 参数sizeCtl = -1 时,表示数组在初始化。 sizeCtl < -1 时 表示正扩容。
  • sizeCtl < -1时,低32位,记录参与扩容的线程数量。(sizeCtl = -10,表示有9个线程参与扩容)
  • 参数 transferIndex 控制各线程迁移哪些节点,从 n 开始,每来一个线程扩容,就减小 一个步长(stride ),即整个数组被分成若干段,一个线程迁移一小段。
  • 节点迁移时,会将节点加锁,该节点迁移完毕,会将旧数组该节点的 hash 值设置为 -1。
  • 参与数据迁移的线程,退出时sizeCtl 值会相应改变,最后一个线程,会将旧数组全部检查一遍,确认第个节点 的hash值都是 -1,设置相关参数后,退出扩容,至此扩容结束。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值