ConCurrentHashMap并发环境时,如何计数的?—— sumCount()、fullAddCount()

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

文章特别长,本篇将计数拿出来单讲

计数

所谓的计数,指的是 ConCurrentHashMap 存了多少个 键值对。


   public int size() {
       long n = sumCount();
       return ((n < 0L) ? 0 :
               (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
               (int)n);
   }

比如调用 size 方法,返回键值对的个数。这里是把 long 型,强转为 int。

当然还有更为精确的方法,mappingCount


   public long mappingCount() {
       long n = sumCount();
       return (n < 0L) ? 0L : n; // ignore transient negative values
   }

这两个方法,都是调用了 sumCount() 方法。


   final long sumCount() {
       CounterCell[] as = counterCells; CounterCell a;
       long sum = baseCount;
       if (as != null) {
           for (int i = 0; i < as.length; ++i) {
               if ((a = as[i]) != null)
                   sum += a.value;
           }
       }
       return sum;
   }

代码很清晰,比较容易理解,计数来自两部分,一个是 baseCount

另一个是 各个 CounterCell 的和。

  /**
   * Base counter value, used mainly when there is no contention,
   * but also as a fallback during table initialization
   * races. Updated via CAS.
   */
  private transient volatile long baseCount;
  
  /**
   * Table of counter cells. When non-null, size is a power of 2.
   */
  private transient volatile CounterCell[] counterCells;

  /**
   * A padded cell for distributing counts.  Adapted from LongAdder
   * and Striped64.  See their internal docs for explanation.
   */
  @sun.misc.Contended static final class CounterCell {
      volatile long value;
      CounterCell(long x) { value = x; }
  }

源码中注释写的很清楚,counterCells 大小是 2 的 n 次方。

CounterCell 这个内部类中的 成员变量只有一个,且是用 vllatile 修饰的。

@sun.misc.Contended 这个可以解决**伪共享**的问题,本文不再展开讲。

以上代码很容易理解,计数方法也没有要讲的。

这里,重点说说,counterCells 是如何初始化的,它的工作原理

以及在多线程环境下,是如何计数的。

重点说明:

先明确一点,计数时,要么修改了 baseCount,要么 修改了 CounterCell 对象中 value的值

put() 方法 中调用了 addCount(1L, binCount); 这个方法。

另外在删除元素 remove 方法时,也调用了 addCount(-1L, -1); 这个方法。

addCount() 这个方法其中一个重要功能,就是计数。


 private final void addCount(long x, int check) {
     CounterCell[] as; long b, s;
     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;
         s = sumCount();
     }
     if (check >= 0) {
		// 可能触发扩容,前面讲过了,跟计数无关系,这里省略
     }
 }

假设 counterCells 还没有初始化,现在有 4 个线程,同时执行为个方法,

   if ((as = counterCells) != null ||
       !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) 

(as = counterCells) != null,按假设来说,这个返回 false,看下一个判断。

!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)

四个线程同时执行这行,那只有一个线程会执行成功,

修改 **baseCount**的值,不进入方法体。

其它三个线程执行方法体中的方法。

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

按假设来讲,as == null 返回 true,三个线程都会执行 fullAddCount(x, uncontended);

这个方法就是进行精确的计数的。等会再细讲。

现在假设 counterCells 已经初始化,且 size 大于0。

还是 4 个线程同时执行 addCount 方法。

   if ((as = counterCells) != null ||
       !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) 

(as = counterCells) != null,按假设来说,这个返回 true,

后半个判断就不会执行了,四个线程都进入方法体。

四个线程都会执行 (a = as[ThreadLocalRandom.getProbe() & m]) == null

ThreadLocalRandom.getProbe() 这个方法是返回一个随机数,彼此之间不同。

as[ThreadLocalRandom.getProbe() & m] 这个是得到数组中的一个元素。

求下标的公式和 HashMap 的一样,这里也不展开来说。

每个线程获取的随机数是不一样的,各自算出一个下标。

假设极端情况下,4 个线程计算出的下标是同一个。

若该下标处元素为 null,那 4 个线程,都会执行 fullAddCount(x, uncontended);

若该下标处元素不为null,那 4 个线程都执行

!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))

那有一个线程成功修改 counterCell 中 value 的值,完成这个线程的计数,返回。

剩下的3个线程,都会执行 fullAddCount(x, uncontended);

总结下:

执行 addCount 时计数时,

counterCells 这个数组未初始化, 非竞争条件下,修改 baseCount,否则执行 fullAddCount(x, uncontended);

counterCells 这个数组已经初始化, 非竞争条件下,修改 对应的 counterCell,否则执行 fullAddCount(x, uncontended);

如果上面的都清楚了,咱就开始分析 fullAddCount(x, uncontended); 方法

 private final void fullAddCount(long x, boolean wasUncontended) {
     int h;
     if ((h = ThreadLocalRandom.getProbe()) == 0) {
         ThreadLocalRandom.localInit();      // force initialization
         h = ThreadLocalRandom.getProbe();
         wasUncontended = true;
     }
     boolean collide = false;                // True if last slot nonempty
     for (;;) {
         CounterCell[] as; CounterCell a; int n; long v;
         if ((as = counterCells) != null && (n = as.length) > 0) {
             if ((a = as[(n - 1) & h]) == null) {
                 if (cellsBusy == 0) {            // Try to attach new Cell
                     CounterCell r = new CounterCell(x); // Optimistic create
                     if (cellsBusy == 0 &&
                         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                         boolean created = false;
                         try {               // Recheck under lock
                             CounterCell[] rs; int m, j;
                             if ((rs = counterCells) != null &&
                                 (m = rs.length) > 0 &&
                                 rs[j = (m - 1) & h] == null) {
                                 rs[j] = r;
                                 created = true;
                             }
                         } finally {
                             cellsBusy = 0;
                         }
                         if (created)
                             break;
                         continue;           // Slot is now non-empty
                     }
                 }
                 collide = false;
             }
             else if (!wasUncontended)       // CAS already known to fail
                 wasUncontended = true;      // Continue after rehash
             else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
                 break;
             else if (counterCells != as || n >= NCPU)
                 collide = false;            // At max size or stale
             else if (!collide)
                 collide = true;
             else if (cellsBusy == 0 &&
                      U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
                 try {
                     if (counterCells == as) {// Expand table unless stale
                         CounterCell[] rs = new CounterCell[n << 1];
                         for (int i = 0; i < n; ++i)
                             rs[i] = as[i];
                         counterCells = rs;
                     }
                 } finally {
                     cellsBusy = 0;
                 }
                 collide = false;
                 continue;                   // Retry with expanded table
             }
             h = ThreadLocalRandom.advanceProbe(h);
         }
         else if (cellsBusy == 0 && counterCells == as &&
                  U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
             boolean init = false;
             try {                           // Initialize table
                 if (counterCells == as) {
                     CounterCell[] rs = new CounterCell[2];
                     rs[h & 1] = new CounterCell(x);
                     counterCells = rs;
                     init = true;
                 }
             } finally {
                 cellsBusy = 0;
             }
             if (init)
                 break;
         }
         else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
             break;                          // Fall back on using base
     }
 }

代码看起来很长,但分解开来,没有特别难的。


  if ((h = ThreadLocalRandom.getProbe()) == 0) {
      ThreadLocalRandom.localInit();      // force initialization
      h = ThreadLocalRandom.getProbe();
      wasUncontended = true;
  }
    

这段是为了让线程获取随机数,先让其初始化。

具体细节就不展开讲了,有兴趣的,可以看下 ThreadLocalRandom 源码。


 for (;;) {
     CounterCell[] as; CounterCell a; int n; long v;
     if ((as = counterCells) != null && (n = as.length) > 0) {
         // 修改某个CounterCell,有可能会对 counterCells 进行扩容
     }
     else if (cellsBusy == 0 && counterCells == as &&
              U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
		// 初始化 counterCells
     }
     else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
         break;                          // Fall back on using base
         // 修改 baseCount 的值
 }

这整体是一个无限循环,三个分支执行成功一个,就可以跳出循环。

否则一直执行。最终的结果是 要么修改了 baseCount ,要么 修改了 CounterCell

先看第二个分支,初始化 counterCells

   CounterCell[] as; CounterCell a; int n; long v;
  if ((as = counterCells) != null && (n = as.length) > 0){
  ……
  }
  else if (cellsBusy == 0 && counterCells == as &&
           U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
      boolean init = false;
      try {                           // Initialize table
          if (counterCells == as) {
              CounterCell[] rs = new CounterCell[2];
              rs[h & 1] = new CounterCell(x);
              counterCells = rs;
              init = true;
          }
      } finally {
          cellsBusy = 0;
      }
      if (init)
          break;
  }

cellsBusy == 0 && counterCells == as 这两个判断是并发的控制。

U.compareAndSwapInt(this, CELLSBUSY, 0, 1) 这个是CAS修改 cellsBusy

cellsBusy 是 1 指在初始化或是在操作 CounterCells

执行成功的那个线程,初始化 counterCells


    /**
     * Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
     */
    private transient volatile int cellsBusy;
    

然后看方法体


  boolean init = false;
  try {                           // Initialize table
      if (counterCells == as) {
          CounterCell[] rs = new CounterCell[2];
          rs[h & 1] = new CounterCell(x);
          counterCells = rs;
          init = true;
      }
  } finally {
      cellsBusy = 0;
  }
  if (init)
      break;
      

counterCell 的初始容量是2,并且把 new 一个 CounterCell 对象,记录了 数值

初始化完毕后,将 cellsBusy 设置为 0;

再看第一个分支


 CounterCell[] as; CounterCell a; int n; long v;
 if ((as = counterCells) != null && (n = as.length) > 0) {
     if ((a = as[(n - 1) & h]) == null) {
         if (cellsBusy == 0) {            // 当前没有线程操作数组
             CounterCell r = new CounterCell(x); // new 一个对象出来
             if (cellsBusy == 0 &&
                 U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { // CAS 设置 cellsBusy 为1
                 boolean created = false; // 开关
                 try {               // Recheck under lock
                     CounterCell[] rs; int m, j;
                     if ((rs = counterCells) != null &&
                         (m = rs.length) > 0 &&
                         rs[j = (m - 1) & h] == null) { // 确保数组存在,对应下标处为空
                         rs[j] = r; // 对象放数组里
                         created = true; // 开关
                     }
                 } finally {
                     cellsBusy = 0; // 操作完后,一定将 cellsBusy 改回 0 
                 }
                 if (created)
                     break; // 设置到成功的情况下,退出。
                 continue;           // Slot is now non-empty
             }
         }
         collide = false;
     }
     else if (!wasUncontended)       // CAS already known to fail
         wasUncontended = true;      // Continue after rehash
     else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
         break; // 对应下标处不为空,且CAS 记录数据成功,退出
     else if (counterCells != as || n >= NCPU) // 当数组size 大于CPU 数量,不让数组扩容。
         collide = false;            // At max size or stale
     else if (!collide)
         collide = true;
     else if (cellsBusy == 0 &&
              U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) { 
         try { // 执行扩容
             if (counterCells == as) {// Expand table unless stale
                 CounterCell[] rs = new CounterCell[n << 1];
                 for (int i = 0; i < n; ++i)
                     rs[i] = as[i];
                 counterCells = rs;
             }
         } finally {
             cellsBusy = 0;
         }
         collide = false;
         continue;                   // Retry with expanded table
     }
     h = ThreadLocalRandom.advanceProbe(h);
 }

if ((a = as[(n - 1) & h]) == null) 意思是 某下标处元素为空,这个小分支,

总的逻辑就是,线程安全情况下, new 一个 CounterCell 对象,记录了数值,设置到数组中。

这个应该好理解,我把注释写代码里了。

第二个大分支就是想办法设置 CounterCell,必要情况下扩容数组。

第三个大分支更好理解,修改 baseCount


 else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
     break; 
     

fullAddCount 方法 代码虽然很长,但各分支逻辑很清晰。

就是:在充分控制并发的情况下,修改 CounterCell 或是 baseCount

修改不成功,就重试,直到成功为止。

总结

计数总逻辑,通过 CounterCell 或是 baseCount,来保证多线程环境下计数问题。

  • 无竞争条件下,执行 put() 方法时,操作baseCount 实现计数
  • 首次竞争条件下,执行 put()方法,会初始化CounterCell ,并实现计数
  • CounterCell 一旦初始化,计数就优先使用CounterCell
  • 每个线程,要么修改CounterCell 、要么修改baseCount,实现计数
  • CounterCell 在竞争特别严重时,会扩容。(扩容上限与 CPU 核数有关,不会一直扩容)
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值