LongAdder原子操作底层原理

一、LongAdder继承Striped64类

abstract class Striped64 extends Number {
        
        // CPU核数,用来决定cells数组的大小
        static final int NCPU = Runtime.getRuntime().availableProcessors();
    
        // 单元数组,大小为2的次幂
        transient volatile Cell[] cells;
    
        /**
         *  基数,在两种情况下会使用:
         *  1. 没有遇到并发竞争时,直接使用base累加数值
         *  2. 初始化cells数组时,必须要保证cells数组只能被初始化一次(即只有一个线程能对cells初始化),
         *  其他竞争失败的线程会讲数值累加到base上
         */
        transient volatile long base;
    
       /**
        * 自旋锁,通过CAS操作加锁,为0表示cells数组没有处于创建、扩容阶段
        * 为1表示正在创建或者扩展cells数组,不能进行新Cell元素的设置操作
        */
        transient volatile int cellsBusy;
    
 
        // 使用 CAS(Compare And Swap)操作来尝试将对象实例的 BASE 字段从 cmp(期望值)修改为 val(更新值)
        final boolean casBase(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
        }
    
        // 作用是使用 CAS(Compare And Swap)操作来尝试将 CELLSBUSY 字段的值从 0 修改为 1
        final boolean casCellsBusy() {
            return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
        }
    
        // 获取当前线程的hash值
        static final int getProbe() {
            return UNSAFE.getInt(Thread.currentThread(), PROBE);
        }
        
    }

二、LongAdder原理的直观理解

三、LongAdder的public void add(long x)方法

 public void add(long x) {
      Cell[] as;  //  Striped64类的cells数组
      long b;     //  Striped64类的的base值
      long v;     //  期望值
      int m;      //  cells数组的长度
      Cell a;     //  当前线程命中的cell数组中的cell单元格对象
      if ((as = cells) != null ||   // CASE 1
      !casBase(b = base, b + x)) {  // CASE 2
          boolean uncontended = true;
          if (as == null || (m = as.length - 1) < 0 ||  // CASE 3
              (a = as[getProbe() & m]) == null ||       // CASE 4

              !(uncontended = a.cas(v = a.value, v + x))) // CASE 5
              longAccumulate(x, null, uncontended);
      }
  }

首先介绍一下代码中的外层if块的两个条件语句CASE 1和CASE2:

  • 条件语句CASE 1:cells数组不为null,说明存在争用;在不存在争用的时候,cells数组一定为null,一旦对base的cas操作失败,才会初始化cells数组

  • 条件语句CASE 2:如果cells数组为null,表示之前不存在争用,并且此次casBase执行成功,表示基于base成员累加成功,add方法直接返回;如果casBase方法执行失败,
    说明产生了第一次争用冲突,需要对cells数组初始化,此时即将进入内层if块。casBase方法很简单,就是通过UNSAFE类的CAS设置成员变量base的值为base+x(要累加的值)

casBase方法的代码如下:

   /**
   * 使用CAS来更新base值
   */
   final boolean casBase(long cmp, long val) {
     return UNSAFE.compareAndSwapLong(this, BASE, cmp,val);
   }

如果add(long x)方法的CASE 1、CASE 2两种条件满足一个,就继续执行内层if语句块,通过Cell元素进行累加,而不是通过base属性
进行累加。接下来介绍add(long x)方法的内层if语句块的三个条件语句CASE3、CASE 4和CASE5:

  • 条件语句CASE 3:as==null||(m=as.length-1)<0代表cells没有初始化。

  • 条件语句CASE 4:指当前线程的hash值在cells数组映射位置的Cell对象为空,意思是还没有其他线程在同一个位置做过累加操作。

  • 条件语句CASE 5:指当前线程的哈希值在cells数组映射位置的Cell对象不为空,然后在该Cell对象上进行CAS操作,设置其值为v+x(x为该Cell需要累加的值),但是CAS操作失败,表示存在争用。

如果以上三个条件语句CASE 3、CASE 4、CASE5有一个为真,就进入longAccumulate()方法。

流程图:

四、LongAdder类中的longAccumulate()方法

longAccumulate()方法的入参:

  • long x :需要做累加的值,increment调用下,一般默认都是+1

  • LongBinaryOperator fn :默认传递的是null

  • wasUncontended:竞争标识,如果是false则代表有竞争,只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false

    final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        // 当前线程还没有对应的 cell, 需要随机生成一个 hash 值用来将当前线程绑定到 cell
        if ((h = getProbe()) == 0) {
            // 初始化 probe,获取 hash 值
            ThreadLocalRandom.current(); 
            h = getProbe();	
            // 默认情况下 当前线程肯定是写入到了 cells[0] 位置,不把它当做一次真正的竞争
            wasUncontended = true;
        }
        // 表示【扩容意向】,false 一定不会扩容,true 可能会扩容
        boolean collide = false; 
        //自旋
        for (;;) {
            // as 表示cells引用,a 表示当前线程命中的 cell,n 表示 cells 数组长度,v 表示 期望值
            Cell[] as; Cell a; int n; long v;
            // 【CASE1】: 表示 cells 已经初始化了,当前线程应该将数据写入到对应的 cell 中
            if ((as = cells) != null && (n = as.length) > 0) {
                // CASE1.1: true 表示当前线程对应的索引下标的 Cell 为 null,需要创建 new Cell
                if ((a = as[(n - 1) & h]) == null) {
                    // 判断 cellsBusy 是否被锁
                    if (cellsBusy == 0) {   
                        // 创建 cell, 初始累加值为 x
                        Cell r = new Cell(x);  
                        // 加锁
                        if (cellsBusy == 0 && casCellsBusy()) {
                            // 创建成功标记,进入【创建 cell 逻辑】
                            boolean created = false;	
                            try {
                                Cell[] rs; int m, j;
                                // 把当前 cells 数组赋值给 rs,并且不为 null
                                if ((rs = cells) != null &&
                                    (m = rs.length) > 0 &&
                                    // 再次判断防止其它线程初始化过该位置,当前线程再次初始化该位置会造成数据丢失
                                    // 因为这里是线程安全的判断,进行的逻辑不会被其他线程影响
                                    rs[j = (m - 1) & h] == null) {
                                    // 把新创建的 cell 填充至当前位置
                                    rs[j] = r;
                                    created = true;	// 表示创建完成
                                }
                            } finally {
                                cellsBusy = 0;		// 解锁
                            }
                            if (created)			// true 表示创建完成,可以推出循环了
                                break;
                            continue;
                        }
                    }
                    collide = false;
                }
                // CASE1.2: 条件成立说明线程对应的 cell 有竞争, 改变线程对应的 cell 来重试 cas
                else if (!wasUncontended)
                    wasUncontended = true;
                // CASE 1.3: 当前线程 rehash 过,如果新命中的 cell 不为空,就尝试累加,false 说明新命中也有竞争
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                    break;
                // CASE 1.4: cells 长度已经超过了最大长度 CPU 内核的数量或者已经扩容
                else if (n >= NCPU || cells != as)
                    collide = false; 		// 扩容意向改为false,【表示不能扩容了】
                // CASE 1.5: 更改扩容意向,如果 n >= NCPU,这里就永远不会执行到,case1.4 永远先于 1.5 执行
                else if (!collide)
                    collide = true;
                // CASE 1.6: 【扩容逻辑】,进行加锁
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        // 线程安全的检查,防止期间被其他线程扩容了
                        if (cells == as) {     
                            // 扩容为以前的 2 倍
                            Cell[] rs = new Cell[n << 1];
                            // 遍历移动值
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            // 把扩容后的引用给 cells
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;	// 解锁
                    }
                    collide = false;	// 扩容意向改为 false,表示不扩容了
                    continue;
                }
                // 重置当前线程 Hash 值,这就是【分段迁移机制】
                h = advanceProbe(h);
            }
    
            // 【CASE2】: 运行到这说明 cells 还未初始化,as 为null
            // 判断是否没有加锁,没有加锁就用 CAS 加锁
            // 条件二判断是否其它线程在当前线程给 as 赋值之后修改了 cells,这里不是线程安全的判断
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                // 初始化标志,开始 【初始化 cells 数组】
                boolean init = false;
                try { 
                   	// 再次判断 cells == as 防止其它线程已经提前初始化了,当前线程再次初始化导致丢失数据
                    // 因为这里是【线程安全的,重新检查,经典 DCL】
                    if (cells == as) {
                        Cell[] rs = new Cell[2];	// 初始化数组大小为2
                        rs[h & 1] = new Cell(x);	// 填充线程对应的cell
                        cells = rs;
                        init = true;				// 初始化成功,标记置为 true
                    }
                } finally {
                    cellsBusy = 0;					// 解锁啊
                }
                if (init)
                    break;							// 初始化成功直接跳出自旋
            }
            // 【CASE3】: 运行到这说明其他线程在初始化 cells,当前线程将值累加到 base,累加成功直接结束自旋
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break; 
        }
    }
    

longAccumulate的自旋过程中,有三个大的if分支:

  • CASE1:表示cells已经初始化了,当前线程应该将数据写入对应的Cell中。

  • CASE2:cells还未初始化(as为null),本分支计划初始化cells,在此之前开始执行cellsBusy加锁,并且要求cellsBusy加锁成功。

  • CASE3:如果cellsBusy加锁失败,表示其他线程正在初始化cells,所以当前线程将值累加到base上。


CASE1表示当前线程应该将数据写入对应的Cell中,又分为以下几种细分情况:
  • CASE1.1:表示当前线程对应的下标位置的Cell为null,需要创建新Cell。

  • CASE1.2:wasUncontended是add(…)方法传递进来的参数如果为false,就表示cells已经被初始化,并且线程对应位置的Cell元素也已经被初始化,但是当前线程对Cell元素的竞争修改失败。如果add方法中的条件语句CASE 5通过CAS尝试把cells[m%cells.length]位置的Cell对象的value值设置为v+x而失败了,就说明已经发生竞争,就将wasUncontended设置为false。如果wasUncontended为false,就需要重新计算prob的值,那么自旋操作进入下一轮循环。

  • CASE 1.3:无论执行CASE1分支的哪个子条件,都会在末尾执行h=advanceProb()语句rehash出一个新哈希值,然后命中新的Cell,如果新命中的Cell不为空,在此分支进行CAS更新,将Cell的值更新为a.value+x,如果更新成功,
    就跳出自旋操作;否则还得继续自旋。

  • CASE 1.4:调整cells数组的扩容意向,然后进入下一轮循环。如果n≥NCPU条件成立,就表示cells数组大小已经大于等于CPU核数,扩容意向改为false,表示不扩容了;如果该条件不成立,就说明cells数组还可以扩容,尽管如此,
    如果cells != as为true,就表示其他线程已经扩容过了,也会将扩容意向改为false,表示当前循环不扩容了。当前线程调到CASE1分支的末尾执行rehash操作重新计算prob的值,然后进入下一轮循环。

  • CASE 1.5:如果!collide=true满足,就表示扩容意向不满足,设置扩容意向为true,但是不一定真的发生扩容,然后进入CASE1分支末尾重新计算prob的值,接着进入下一轮循环。

  • CASE 1.6:执行真正扩容的逻辑。其条件一cellsBusy==0为true表示当前cellsBusy的值为0(无锁状态),当前线程可以去竞争这把锁;其条件二casCellsBusy()表示当前线程获取锁成功,CAS操作cellsBusy改为0成功,可以执行扩容逻辑。

流程图:

五、LongAdder类的casCellsBusy()方法

casCellsBusy()方法的代码很简单,就是将cellsBusy成员的值改为1,表示目前的cells数组在初始化或扩容中,具体的代码如下:

final boolean casCellsBusy() {
   return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0,1);
}

casCellsBusy()方法相当于锁的功能:当线程需要cells数组初始化或扩容时,需要调用casCellsBusy()方法,通过CAS方式将cellsBusy成员的值改为1,如果修改失败,就表示其他的线程正在进行数组初始化或扩容的操作。只有CAS操作成功,cellsBusy成员的值被改为1,当前线程才能执行cells数组初始化或扩容的操作。在cells数组初始化或扩容的操作执行完成之后,cellsBusy成员的值被改为0,这时不需要进行CAS修改,直接修改即可,因为不存在争用。当cellsBusy成员值为1时,表示cells数组正在被某个线程执行初始化或扩容操作,其他线程不能进行以下操作:

  • 对cells数组执行初始化。
  • 对cells数组执行扩容。
  • 如果cells数组中某个元素为null,就为该元素创建新的Cell对象。因为数组的结构正在修改,所以其他线程不能创建新的Cell对象。
posted @   jock_javaEE  阅读(8)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示