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对象。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律