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 @ 2024-06-26 17:28  jock_javaEE  阅读(74)  评论(0)    收藏  举报