LongAdder In Java8
概述
LongAdder
在多线程竞争较大的环境下,用于替代AtomicLong
。两者底层都是使用CAS来update底层long变量的值,然而当多线程竞争较激烈的情况下时,AtomicLong
会造成很多CPU空轮询(竞争太激烈,cas总是失败);而LongAdder
采用将一个long变量的存储和更新分散到多个Cell中的做法:每个线程对应特定的一个cell,再进行更新时只更新本线程对应cell的long值,当需要获取累加值时,再通过将全部cell对应long值累加的方式,来减少线程间的竞争。
首先,将数据分段处理的做法并不是LongAdder
实现的,而是它的父类Striped64
实现的,基于此类还有LongAccumulator
等。(LongAccumulator
类提供了自定义Function
的能力,比如可以传入一个做乘法的Function,即可实现Long的累乘器)。
源码解读
下面的文章推荐先通读一遍,对LongAdder和LongAccumulator有个初步了解,对于LongAdder是咋用的有了印象,再看具体实现。🤭
先从Striped64
开始。上文说到该类将一个64位数据分散到多个cell中去,对应的就是Striped64
的内部类Cell
,类定义如下:
static final class Cell {
// volatie保证可见性,如果是要存储double变量,则将double转化为对应的long(底层64位bit相同)
volatile long value;
Cell(long x) { value = x; }
// cas操作保证多线程安全
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// 用于进行cas操作
private static final sun.misc.Unsafe UNSAFE;
// value变量在cell对象对应内存起始地址的偏移量,用于cas操作
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
然后是Striped64
类中比较重要的几个field:
// 上述Cell类的数组,存储用于线程对应的Cell对象
transient volatile Cell[] cells;
// 逻辑cpu的数量,也是cells数组的最大长度
static final int NCPU = Runtime.getRuntime().availableProcessors();
// base变量,相当于Striped64类存储的一个默认Cell对象
// 当线程竞争不那么激烈时,对base值进行cas即可。因此在竞争不激烈时,该类等同于AtomicLong
transient volatile long base;
// cells数组对应的cas乐观锁,当需要对cells数组进行初始化、扩容时,需要先获得这个锁(即cas为1)
transient volatile int cellsBusy;
// 这几个变量用于cas操作,和Cell中的valueOffset类似
private static final sun.misc.Unsafe UNSAFE;
// base变量对于Striped64对象内存起始位置的偏移量
private static final long BASE;
// cellsBusy变量对于Striped64对象内存起始位置的偏移量
private static final long CELLSBUSY;
// base变量对于Thread对象内存起始位置的偏移量,该变量名为threadLocalRandomProbe,定义在Thread类中
// threadLocalRandomProbe变量可以认为是每个线程唯一的一个随机数,具体实现机制参考ThreadLocalRandom
private static final long PROBE;
有了cells对象,那么当多线程竞争时,每个线程都可以获取到自己对应的Cell对象来进行cas更新,相当于多个线程的竞争分散到了多个位置上。那么每个线程是如何获取自身对应的Cell对象的呢?
相关源码如下:
// 获取probe值
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
// 调整probe值
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
得到线程对于的probe值之后,再使用number & (cells.length - 1)
的hash运算,来获取到对应cell对象的下标,在进行对cell对象的操作。
Striped64
初始化时,底层的cells数组为null,因此再一开始没啥竞争的时候,通过对base变量的cas操作也就满足需求了。也就是下面这个:
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
只有当发生多线程竞争时,才开始初始化cells数组,具体的方法是longAccumulate(long, LongBinaryOperator, boolean)
,对于double来说是doubleAccumulate
方法,这里以long为例分析源码:
看这里的源码时需要注意的一点时,在条件语句中,虽然有可能条件判断结果是false,但是赋值语句已经执行了。这种写法在concurrent包下很常见。
// x是要通过fn对底层的long值进行操作的变量
// wasUncontended如果为true,表示没有竞争
final void doubleAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 如果获取到的probe值为0,说明该Thread未初始化此值,则强制初始化一次
// 具体的初始化流程可参考ThreadLocalRandom
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current();
h = getProbe();
// 新来的线程,乐观的认为有很大几率申请到自己独属的Cell对象或者casBase成功,则先置为没竞争
wasUncontended = true;
}
// 表示当前cells数组是否已满
boolean collide = false;
// cas无限循环
for (;;) {
Cell[] as; Cell a; int n; long v;
// 1.cas已初始化
if ((as = cells) != null && (n = as.length) > 0) {
// *************************************1-1*************************************
// 获取当前线程对应的Cell对象,如果为空
if ((a = as[(n - 1) & h]) == null) {
// cells锁
if (cellsBusy == 0) {
// 创建新的Cell
Cell r = new Cell(x);
// 通过cas来获取cells数组锁
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 二次检查,这里避免在进入该分支之后cells已经被update过
// 或者hash出来的位置已经被填充
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// *************************************1-1*************************************
// *************************************1-2*************************************
// 能进入这个分支则wasUncontented=false,也就是说认为有竞争
// 认为有竞争的话就先不要去和其他线程竞争了,先等一回合
// 下一回合应该就没竞争了, 即wasUncontented=true
// 如果进不来这个分支的话,就说明认为没竞争(实际上有竞争也该去尝试了)
else if (!wasUncontended)
wasUncontended = true;
/*
* 这里变更竞争标识之后,会直接到最下面的h = advanceProbe(h),即调整probe变量值,
* 重新进行hash计算。下面的分支同理。
*/
// *************************************1-2*************************************
// *************************************1-3*************************************
// 存在竞争,通过hash获取的Cell对象不为空
// 尝试使用已存在的Cell对象来进行cas操作
// 如果操作成功,则break循环
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// *************************************1-3*************************************
// *************************************1-4*************************************
// 存在竞争、通过hash获取的Cell对象不为空、对Cell进行cas失败
// cells数组已经到了最大容量,或者cells数组不等于前面获取到的as局部变量(调整过length)
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// *************************************1-4*************************************
// *************************************1-5*************************************
/*
* 能走到这个位置说明上面几个条件测试都失败了,也就是以下几点:
* (1)hash获取到的slot不为空
* (2)存在竞争
* (3)对获取到的Cell对象进行cas失败
* (4)cells数组长度小于cpu数量或者cells数组没更新
*
* 如果之前认为cells数组未满,则此时此刻可以认为cells数组已经被占满了,需要对cells数组
* 进行扩容
*
* 到下一轮循环之后,如果上述四个条件还满足(前面的条件全都测试失败),就会进入下面的cells
* 数组扩容环节需要注意的是,如果当前数组的长度n已经通过扩容到达了NCPU,那么之后在也不会进
* 行扩容了,因为每次都会进入到上面n >= NCPU的条件分支去
*/
else if (!collide)
collide = true;
// *************************************1-5*************************************
// *************************************1-6*************************************
// 通过cas获取cells数组的锁
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// double check,避免cells数组长度已经更新
if (cells == as) {
// 扩展到2倍
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// *************************************1-6*************************************
// 调整h的大小
h = advanceProbe(h);
}
// 2.cas未初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 3.在对cells进行操作的间隙,再次尝试用base变量来cas,如果成功则退出
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
}
}
到这里对Striped64
的分析就结束了,那么再回头看LongAdder
其实就简单多了,说白了,其实就是提供了一个累加或累减的工具类,实际的工作都是在父类中干的。再简单分析下LongAdder
中比较重要的add(long)
方法,源码如下:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null // cells数组有没初始化,即之前有没有竞争
|| !casBase(b = base, b + x)) { // 先通过casBase尝试一把,能不能直接成功,如果成功的话就不走下面了
// 乐观认为没竞争
boolean uncontended = true;
if (as == null // cells数组没初始化
|| (m = as.length - 1) < 0 // cells数组长度小于等于0
|| (a = as[getProbe() & m]) == null || // 当前线程对应的Cell是null
// 对当前线程对应cell尝试进行cas,成功就退出,不成功就将uncontented置为false(认为有竞争)
// 进入父类的longAccumulate方法中
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
总结
LongAdder
和AtomicLong
都提供了多线程环境下long变量运算的线程安全,但是LongAdder
类使用空间换时间的方法,来减少多线程之间的竞争,大大提高了多线程下long运算的效率,ConcurrentHashMap
在java1.8中的实现也是类似的。
感觉不错的看官可以点个👍:)