ConcurrentHashmap的addCount(2)

判断是否需要扩容,也就是当更新后的键值对总数baseCount >= 阈值sizeCtl时,进行rehash,这里面会有两个逻辑。

/**
  * 在putVal最后调用addCount的时候,传递了两个参数,分别是1和binCount(链表长度)
  * x表示这次需要在表中增加的元素个数,check参数表示是否需要进行扩容检查,大于等于0都需要进行检查
 */
private final void addCount(long x, int check) {

        CounterCell[] as; long b, s;

        /**
          * 判断counterCells是否为空,

          * 1. 如果为空,就通过cas操作尝试修改baseCount变量,对这个变量进行原子累加操作(做这个操作的意义是:如果在没有竞争的情况下,仍然采用baseCount来记录元素个数)

          * 2. 如果cas失败说明存在竞争,这个时候不能再采用baseCount来累加,而是通过CounterCell来记录
         */
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;

            //是否冲突标识,默认为没有冲突
            boolean uncontended = true;

            这里有几个判断

            //1. 计数表为空则直接调用fullAddCount

            //2. 从计数表中随机取出一个数组的位置为空,直接调用fullAddCount

            //3. 通过CAS修改CounterCell随机位置的值,如果修改失败说明出现并发情况(这里又用到了一种巧妙的方法),调用fullAndCount

            //Random在线程并发的时候会有性能问题以及可能会产生相同的随机数,ThreadLocalRandom.getProbe可以解决这个问题,并且性能要比Random高
            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;
            }

            //链表长度小于等于1,不需要考虑扩容
            if (check <= 1)
                return;

            //统计ConcurrentHashMap元素个数
            s = sumCount();
        }

        /**
          * 如果当前正在处于扩容阶段,则当前线程会加入并且协助扩容
          * 如果当前没有在扩容,则直接触发扩容操作
        */
        //如果binCount>=0,标识需要检查扩容
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;

            //s标识集合大小,如果集合大小大于或等于扩容阈值(默认值的0.75)
            //并且table不为空并且table的长度小于最大容量
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {

                //这里是生成一个唯一的扩容戳,这个是干嘛用的呢?且听我慢慢分析
                int rs = resizeStamp(n);

                //sc<0,也就是sizeCtl<0,说明已经有别的线程正在扩容了
                if (sc < 0) {

                    //这5个条件只要有一个条件为true,说明当前线程不能帮助进行此次的扩容,直接跳出循环

                    //sc >>> RESIZE_STAMP_SHIFT!=rs 表示比较高RESIZE_STAMP_BITS位生成戳和rs是否相等,相同

                    //sc=rs+1 表示扩容结束

                    //sc==rs+MAX_RESIZERS 表示帮助线程线程已经达到最大值了

                    //nt=nextTable -> 表示扩容已经结束

                    //transferIndex<=0 表示所有的transfer任务都被领取完了,没有剩余的hash桶来给自己自己好这个线程来做transfer
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;

                    //当前线程尝试帮助此次扩容,如果成功,则调用transfer
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }

                // 如果当前没有在扩容,那么rs肯定是一个正数,通过rs<<RESIZE_STAMP_SHIFT 将sc设置为一个负数,+2 表示有一个线程在执行扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);

                // 重新计数,判断是否需要开启下一轮扩容
                s = sumCount();
            }
        }
    }

resizeStamp

这块逻辑要理解起来,也有一点复杂。 resizeStamp用来生成一个和扩容有关的扩容戳,具体有什么作用呢?我们基于它的实现来做一个分析

  1. static final int resizeStamp(int n) {

  2. return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));

  3. }


  1. Integer.numberOfLeadingZeros 这个方法是返回无符号整数n最高位非0位前面的0的个数

  2. 比如10的二进制是 0000 0000 0000 0000 0000 0000 0000 1010

  3. 那么这个方法返回的值就是28

  4. 根据resizeStamp的运算逻辑,我们来推演一下,假如n=16,那么resizeStamp(16)=32796转化为二进制是

  5. [0000 0000 0000 0000 1000 0000 0001 1100]

  6. 接着再来看,当第一个线程尝试进行扩容的时候,会执行下面这段代码

  7. U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)

  8. rs左移16位,相当于原本的二进制低位变成了高位1000 0000 0001 1100 0000 0000 0000 0000

  9. 然后再+2 =1000 0000 0001 1100 0000 0000 0000 0000+10=1000 0000 0001 1100 0000 0000 0000 0010

  10. 高16位代表扩容的标记、低16位代表并行扩容的线程数

  11.  

  12.  这样来存储有什么好处呢?

  13. 1. 首先在CHM中是支持并发扩容的,也就是说如果当前的数组需要进行扩容操作,可以由多个线程来共同负责,这块后续会单独讲

  14. 2. 可以保证每次扩容都生成唯一的生成戳,每次新的扩容,都有一个不同的n,这个生成戳就是根据n来计算出来的一个数字,n不同,这个数字也不同

  15.  第一个线程尝试扩容的时候,为什么是+2

  16. 因为1表示初始化,2表示一个线程在执行扩容,而且对sizeCtl的操作都是基于位运算的,所以不会关心它本身的数值是多少,只关心它在二进制上的数值,而sc + 1会在

  17. 低16位上加1。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值