Fork me on GitHub

ConcurrentHashMap原理分析(三)-计数

概述

  由于ConcurrentHashMap是一个高并发的集合,集合中增删就比较频繁,那计数就变成了一个问题,如果使用像AtomicInteger这样类型的变量来计数,虽然可以保证原子性,但是太多线程去竞争CAS,自旋也挺浪费时间的,所以ConcurrentHashMap使用了一种类似LongAddr的数据结构去计数,其实LongAddr是继承Striped64,有关于这个类的原理大家可以参考这篇文章:并发之STRIPED64(累加器)和 LONGADDER,大家了解了这个类的原理,理解ConcurrentHashMap计数就没有一点压力了,因为两者在代码实现上基本一样。

ConcurrentHashMap计数原理

    private transient volatile long baseCount;

    private transient volatile CounterCell[] counterCells;

    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

ConcurrentHashMap就是依托上面三个东东进行计数的,那下面就详细解释一下这三个东东。

  • baseCount:最基础的计数,比如只有一个线程put操作,只需要通过CAS修改baseCount就可以了。
  • counterCells:这是一个数组,里面放着CounterCell对象,这个类里面就一个属性,其使用方法是,在高并发的时候,多个线程都要进行计数,每个线程有一个探针hash值,通过这个hash值定位到数组桶的位置,如果这个位置有值就通过CAS修改CounterCell的value(如果修改失败,就换一个再试),如果没有,就创建一个CounterCell对象。
  • 最后通过把桶中的所有对象的value值和baseCount求和得到总值,代码如下。
    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        //baseCount作为基础值
        long sum = baseCount;
        if (as != null) {
            //遍历数组
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    //对每个value累加
                    sum += a.value;
            }
        }
        return sum;
    }

通过上面的分析,相信大家已经了解了高并发计数的方法,在上面的介绍中提到一点,每个线程的探针hash值,大家先有个印象,一会分析代码的时候会使用这个,其实这个值很有趣。

addCount()方法

又到了这个方法,在上篇文章中分析扩容的时候也分析过这个方法,不过分析的是一部分,现在分析另一部分。

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        //如果数组还没有创建,就直接对baseCount进行CAS操作
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            //设置没有冲突标志位为true
            boolean uncontended = true;
            //第一个条件as == null成立说明,counterCells数组没有创建,而且通过CAS修改baseCount失败,说明有别的线程竞争CAS
            //a = as[ThreadLocalRandom.getProbe() & m]) == null,说明数组是创建了,但是通过探针hash定位的桶中没有对象
            //如果有对象,执行最后一个,进行CAS修改CounterCell对象,如果也失败了,就要进入下面的方法
            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();
        }
        //省略部分代码
}

上面整体过程还是很好理解的,就是我在上面介绍计数原理中说的步骤,但是上面有一个地方需要注意下就是:

a = as[ThreadLocalRandom.getProbe() & m]) == null 

这里的ThreadLocalRandom.getProbe(),看着名字大家应该可以猜到应该是要获取一个随机值,因为有Random嘛,其实这里就是在获取一个随机值。那既然是要获取一个随机值,为什么要使用ThreadLocalRandom,而不直接使用Random,那看到ThreadLocal,大家也应该可以想到就是这个随机值的获取,线程之间是隔离的,每个线程获取自己的随机值,互相之间没有影响,为什么要互相之间没有影响呢?因为Random要实现随机,有一个关键的东西就是种子(seed),具体过程如下:

  • 初始化Random的时候,如果没有传seed,就根据时间戳进行一些计算初始化一个种子
  • 如果某个线程需要获取随机数,先通过CAS更新种子seed1 = function1(seed)
  • 根据seed1,计算随机数 = function2(seed1)
  • 上面的两个function是固定的,就是说如果初始种子一样,两个不同的Random对象生成随机数会完全一样

上面的过程咋一看没啥问题,其实最大问题就是第二步那个CAS,在高并发的时候效率会很差,所以这里才使用了ThreadLocalRandom,相当于每个线程都有一个Random,都有自己的种子,这样就不会存在多线程竞争修改种子。想要详细了解ThreadLocalRandom,参考:并发包中ThreadLocalRandom类原理剖析

fullAddCount()方法

其实这个方法没什么好分析的,其实就是Striped64#longAccumulate()方法,据我的观察好像一行不差,完全一样,这里还是分析下吧。

  1 private final void fullAddCount(long x, boolean wasUncontended) {
  2         int h;
  3          //上面我贴出来了介绍ThreadLocalRandom的文章,这里如果是首次获取,其实就是0 
  4          if ((h = ThreadLocalRandom.getProbe()) == 0) {
  5             //如果为0,就初始化,这里其实就是把种子和随机数设置到(Thread)线程中
  6             ThreadLocalRandom.localInit();      // force initialization
  7             h = ThreadLocalRandom.getProbe();
  8             wasUncontended = true;
  9         }
 10         boolean collide = false;                // True if last slot nonempty
 11      
 12         //死循环,保证计数一定成功
 13         for (;;) {
 14             CounterCell[] as; CounterCell a; int n; long v;
 15             //说明数组已经初始化,在后面有判断数组没有初始化的情况
 16             if ((as = counterCells) != null && (n = as.length) > 0) {
 17                 //这里是不是和ConcurrentHashMap定位桶的位置很像,其实是一摸一样的
 18                //说明数组中这个位置没有元素 
 19                if ((a = as[(n - 1) & h]) == null) {
 20                     //这个字段保证数组新增节点,扩容只有一个线程在进行,防止多线程并发
 21                     //这里限制一个线程处理只是在数组新增节点和扩容的时候,修改对象的值并不需要限制这个变量
 22                     if (cellsBusy == 0) {            // Try to attach new Cell
 23                         CounterCell r = new CounterCell(x); // Optimistic create
 24 
 25                         //如果为0表示没有别的线程在修改数组,通过CAS修改为1,表示当前线程在修改数组
 26                         if (cellsBusy == 0 &&
 27                             U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
 28                             boolean created = false;
 29                             try {               // Recheck under lock
 30                                 CounterCell[] rs; int m, j;
 31                                 //再次校验,确保数组没有变化
 32                                 //rs[j = (m - 1) & h] == null,再次确认该位置是否为null,防止别的线程插入了
 33                                 if ((rs = counterCells) != null &&
 34                                     (m = rs.length) > 0 &&
 35                                     rs[j = (m - 1) & h] == null) {
 36                                     //插入数组
 37                                     rs[j] = r;
 38                                     created = true;
 39                                 }
 40                             } finally {
 41                                //释放CAS锁
 42                                 cellsBusy = 0;
 43                             }
 44                             if (created)
 45                                 //如果新节点插入成功,表示计数已经成功,这里直接break了
 46                                 break;
 47                            //如果失败会一直重试
 48                             continue;           // Slot is now non-empty
 49                         }
 50                     }
 51                     collide = false;
 52                 }
 53                 else if (!wasUncontended)       // CAS already known to fail
 54                     wasUncontended = true;      // Continue after rehash
 55               
 56                 //定位到桶中有值,然后通过CAS修改其值
 57                 else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
 58                     break;
 59                //下面的两个elseif其实是为了防止数组一直扩容使用的,数组的最大容量就是CPU的核数
 60                //因为核数就是并发数,数组太大没有意义,没有那么多线程可以同时操作
 61                //就是说上面的新建节点或者CAS修改值事变了,就会到这里,然后拦截住,不让执行扩容
 62                 else if (counterCells != as || n >= NCPU)
 63                     collide = false;            // At max size or stale
 64                 else if (!collide)
 65                     collide = true;
 66                 //先竞争到CAS锁,然后执行扩容
 67                 else if (cellsBusy == 0 &&
 68                          U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
 69                     try {
 70                         if (counterCells == as) {// Expand table unless stale
 71                            
 72                             //每次扩容成原来的两倍
 73                             CounterCell[] rs = new CounterCell[n << 1];
 74                             //复制元素,看过ConcurrentHashMap的扩容,再看这个,简直就跟一个大学生看小学数学题一样,😄
 75                             for (int i = 0; i < n; ++i)
 76                                 rs[i] = as[i];
 77                             counterCells = rs;
 78                         }
 79                     } finally {
 80                         cellsBusy = 0;
 81                     }
 82                     collide = false;
 83                     continue;                   // Retry with expanded table
 84                 }
 85                 //这里是重新生成一个随机数,换个位置试试,比如上面新增节点失败了,换个位置试试,或者通过CAS修改值失败,也换个位置再试试
 86                 h = ThreadLocalRandom.advanceProbe(h);
 87             }
 88             //这里就是判断数组没有初始化的情况,搞不明白没啥放在这里,不放在开头
 89             else if (cellsBusy == 0 && counterCells == as &&
 90                      U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
 91                 boolean init = false;
 92                 try {                           // Initialize table
 93                     if (counterCells == as) {
 94                         //初始化的数组大小是2,非常小
 95                         CounterCell[] rs = new CounterCell[2];
 96                         rs[h & 1] = new CounterCell(x);
 97                         counterCells = rs;
 98                         init = true;
 99                     }
100                 } finally {
101                     cellsBusy = 0;
102                 }
103                 if (init)
104                     break;
105             }
106             //如果以上CAS修改,创建新节点都失败了,这里还有一道防线,通过CAS修改baseCount
107             //这也是再addCount中,当判断数组不为空,不先修改下baseCount试试,而是直接跳到这个方法中,因为在这个方法中也会修改baseCount
108             else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
109                 break;                          // Fall back on using base
110         }
111     }

方法流程梳理如下:

  1. 判断数组是否为空,为空的话初始化数组
  2. 如果数组存在,通过探针hash定位桶中的位置,如果桶中为空,新建节点,通过CAS锁插入数组,如果成功,结束,如果失败转到第5步
  3. 如果定位到桶中有值,通过CAS修改,如果成功,结束,如果失败向下走
  4. 如果数组大小小于CPU核数,扩容数组
  5. 重新计算探针hash

总结

  计数的原理就是概述中说的使用的是striped64,为啥不直接继承striped64,不太懂,可能striped64出来晚一点,里面使用到ThreadLocalRandom,这个其实还是挺有意思的,总的来说计数过程并不复杂,是看ConcurrentHashMap源码的时候比较愉快的部分。

    

posted @ 2020-09-11 19:11  猿起缘灭  阅读(1879)  评论(1编辑  收藏  举报