LongAdder是JUC包下的一个加法器,可以用于并发下的计数,但是在多线程的情况下其实获得他的值并不是一定是当前正确的值,毕竟他的sum方法里面对于读取数据没有任何的锁,但是LongAdder是可以保证最终正确性的,也就是每一次的加都是有效的。
可能这边会有人有疑问,既然是加法器,那我直接使用AtomicInteger不是也可以实现吗,为什么还要使用LongAdder呢?
确实,我刚开始也会有疑问,所以我们来做个实验,来测测多线程下这两个的耗时
void testLongAdder() throws InterruptedException {
LongAdder longAdder = new LongAdder();
CountDownLatch countDownLatch = new CountDownLatch(5);
long start = System.currentTimeMillis();
for (int n = 0; n < 5; n++) {
new Thread(() -> {
for (int m = 0; m < 500000; m++) {
longAdder.add(1);
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(System.currentTimeMillis() - start);
System.out.println(longAdder.sum());
}
void testAtomicInteger() throws InterruptedException {
AtomicInteger atomicInteger = new AtomicInteger();
CountDownLatch countDownLatch = new CountDownLatch(5);
long start = System.currentTimeMillis();
for (int n = 0; n < 5; n++) {
new Thread(() -> {
for (int m = 0; m < 500000; m++) {
atomicInteger.incrementAndGet();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(System.currentTimeMillis() - start);
System.out.println(atomicInteger.get());
}
上面这两段代码差不多,大体的意思就是开5个线程 每个线程 累加500000次,然后看最后的耗时哪个比较长
直接上最后结果了
AtomicInteger每次耗时都是大约100ms多一些
LongAdder每次耗时都在50ms以下
这个差距会随着累加次数的增加而增加,也会随着线程的增加而增加
那么为什么LongAdder会这么高效呢
我们直接看他源码里面的实现
读源码之前,我们先来看一下LongAdder使用到的几个关键的东西
一个是Cell静态内部类,还有一个base变量
@sun.misc.Contended static final class Cell {
// 当前这个cell里面的值
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
private static final sun.misc.Unsafe UNSAFE;
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);
}
}
}
这就是Cell内部类的定义,主要就是一个value的成员变量用来计数用的
还有一个cas的方法就是使用cas来替换value的值
base变量则是当没有竞争的情况下,我们直接加这个变量,没必要使用cell数组分片
结合下面的源码我们来理解
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 如果当前cell数组不为空 或者 尝试cas 给base变量替换成加成功之后的值
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
// 这边就判断如果cell数组为null 或者 当前线程操作的cell不为null
// 那么尝试给cas给这个cell替换值
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
上面代码也就是判断如果cell数组为null,那么之前肯定没有发生竞争,那么就尝试直接在base变量上加就可以了。至于他是判断当前这个线程是第几个cell,这边他是通过sun.misc.Unsafe 类然后偏移量获得当前线程里面的threadLocalRandomProbe的值(其实在反射底层也是通过这样子来获得成员变量的值),这个我的理解是这个线程的一个随机值。然后这个值 和 cell数组的长度 取模 运算然后定位到对应的cell,如果对应的cell不为null 并且cas失败 那么就进入到longAccumulate方法,我们进入看看
这个代码就有点长了,慢慢阅读
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 这边也就是获得当前这个线程的threadLocalRandomProbe变量的值
// 如果为 0 代表还没被初始化
if ((h = getProbe()) == 0) {
// 初始化这个值
ThreadLocalRandom.current();
h = getProbe();
wasUncontended = true;
}
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// 如果cell 数组不为null并且长度大于0
if ((as = cells) != null && (n = as.length) > 0) {
// 如果这个线程对应的cell为null
if ((a = as[(n - 1) & h]) == null) {
// 通过这个变量来判断当前有没有线程在扩容cell 或者也在初始化cell
if (cellsBusy == 0) {
// 初始化一个cell变量
Cell r = new Cell(x);
// 再次判断 并且使用cas去修改cellsBusy变量 如果修改成功则代表当前线程可以去尝试初始化这个线程对应的cell
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try {
Cell[] rs; int m, j;
// 再次判断当前这个线程对应的cell有没有被初始化
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;
}
}
collide = false;
}
// 这里我的理解就是在之前cas已经失败了就证明有其他线程也对应这个cell 所以就不再尝试cas了 直接在下面修改当前线程的threadLocalRandomProbe的值也就是尝试一个新的cell对应当前线程
else if (!wasUncontended)
wasUncontended = true;
// 如果当前线程的cell 不为null 那么使用cas去尝试加上x
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
// 如果cas成功 就退出
break;
// 如果上一步cas失败 并且当前cell的长度已经大于等于cpu的核心数(NCPU这个变量就是核心数的大小)
// 或者 cell 已经被改变过 其实也就是扩容过了(其实这里满足了也就是不用再往下判断了)
else if (n >= NCPU || cells != as)
collide = false;
// 这里我觉得是给一次机会 不扩容cell数组 默认collide就为false
else if (!collide)
collide = true;
// 这步就是 判断当前状态是否没有线程在扩容或者初始化cell
// 如果没有的话使用cas去修改状态值
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 判断之前没有线程修改cell数组的引用 也就是之前没有线程已经扩容了
if (cells == as) {
// 两倍扩容
Cell[] rs = new Cell[n << 1];
// 将之前的迁移到新的cell数组里面
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue;
}
// 修改当前线程的threadLocalRandomProbe的值也就是尝试一个新的cell对应当前线程
h = advanceProbe(h);
}
// 如果当前cell数组为null 并且状态为0 使用cas尝试修改状态
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try {
// 再次判断cell数组没有被修改过
if (cells == as) {
// 初始化长度为2的数组
Cell[] rs = new Cell[2];
// 根据奇偶初始化一个新的cell
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
// 把状态修改回来
cellsBusy = 0;
}
// 如果初始化成功 就退出循环
if (init)
break;
}
// 如果当前cell数组为null 并且初始化也没有轮到当前线程那就暂时 加到base变量
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
经过上面的源码分析,我们可以总结一下 LongAdder高效的原因
如果没有竞争的情况下 直接使用cas操作base变量
如果在有竞争的情况下,尽量把每个线程的操作分散到不同的cell,这样子就会避免竞争,最优的情况下,如果cpu的核心八个核心,当同时来调用add方法,如果每个核心的线程都是不同的cell那效率就非常高了。如果线程确实多
然后各种冲突他也会进行扩容,但是最多也只是扩到cpu的核心数
其实还有一个细节的点,就是cell类上面还有一个@sun.misc.Contended
这个注解,这个注解是为了解决一种伪共享的情况,这边解释一下这个现象,就是cpu底层有缓存,缓存是以缓存行为单位的(常见的缓存行一行为64个字节),如果不加这个注解,那么这个cell数组可能会一部分在同一个缓存行里面,那么基于MESI协议那么如果cpu更新了这个缓存里的数据就会造成相应的这行的缓存行失效,所以即使 cpu的不同核心更新的是不同cell,那么也会造成缓存行的失效,那么其他核心发现缓存行的失效,就得去主存同步新的数据回来。这样子就会损失效率,@sun.misc.Contended这个注解就是为了解决这个问题,也就是去填充缓存行,使得一行只可以放得下一个cell,也就解决了伪共享的问题