多线程场景下,当我们要对一个整形值进行++操作时,是使用AtomicLong原子类还是使用LongAdder,我们先来看下测试代码,如下:
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(10000);
AtomicLong count = new AtomicLong(0);
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 10000; i1++) {
count.getAndIncrement();
}
latch.countDown();
}).start();
}
latch.await();
System.out.println("AtomicLong 耗时:" + (System.currentTimeMillis() - start) + ",结果值:" + count.get());
longAdderTest();
}
private static void longAdderTest() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10000);
long start;
LongAdder num = new LongAdder();
start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 10000; i1++) {
num.increment();
}
latch.countDown();
}).start();
}
latch.await();
System.out.println("LongAdder 耗时:" + (System.currentTimeMillis() - start) + ",结果值:" + num.longValue());
}
执行结果如下
AtomicLong 耗时:1756,结果值:100000000
LongAdder 耗时:798,结果值:100000000
测试结果发现,随着线程数的增加及类加次数增多,LongAdder性能明显高于AtomicLong,为什呢?接下来我们来分析一下LongAdder的源码:
首先 LongAdder 继承自 Striped64 ,在 Striped64 中我们发现有个 Cell 内部类,代码如下
// @Contended: 缓存行填充,让每个cell独占一个缓存行。如果不使用缓存行填充,多个cell在同一个缓存行中时,多线程竞争时就需要对缓存行进行上锁,
@sun.misc.Contended static final class 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);}
}
}
// Striped64 中的核心变量
// CPU 核心数
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 打散CAS操作
transient volatile Cell[] cells;
// 无线程征用时直接对base变量进行CAS
transient volatile long base;
// 标识cells数组是否繁忙(是否有线程正在使用)
transient volatile int cellsBusy;
此类的作用:提供分散CAS操作,降低锁争用
下面我们来看看LongAdder源码实现:
累加操作,核心就是当存在多线程竞争的时候,将CAS操作分散到不同的cell 中,最后获取总值的时候将各个cell中的值与base的值累加返回
多线程竞争点:
1、cells数组没有初始化
2、cells中 线程随机数对应下标处 cell 没有初始化
上述2中情况都已完成时,尝试对base进行CAS,如果CAS成功直接返回,从而降低CPU空转
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// as = cells 将当前 cells数组引用赋值给 as ,如果 as == null 表名没有线程争用,直接对base变量CAS
// as != null 表名存在线程征用的情况
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || // 多线程下,as 可能已经被修改,
(m = as.length - 1) < 0 || // m 用于计算 as 数组下标
(a = as[getProbe() & m]) == null || // 如果当前下标处的 cell == null,表明此cell 无竞争,直接CAS即可
!(uncontended = a.cas(v = a.value, v + x))) // CAS 失败表明有线程竞争
longAccumulate(x, null, uncontended);
}
}
//
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
// 初始化线程随机数
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current();
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) { // 通过线程随机数获得的数组下标处的cell未初始化
if (cellsBusy == 0) {
// 1、多线程场景下,多个线程同时执行到这一步,开始竞争下面的cellsBusy,
Cell r = new Cell(x);
// 2、CAS cellsBusy 字段 将其设置为 1,成功代表获得锁,可以执行下面cell初始化的操作
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 多线程场景下,cell 可能已经被初始化了(等待在第一步的操作再次获取到锁进入这里时,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; // Slot is now non-empty
}
}
collide = false;
}
// 上一步中CAS操作失败,此时将其设置为 ture ,并重新获取线程随机数让其重新进入循环操作
else if (!wasUncontended)
wasUncontended = true;
// 尝试对该cell再次进行CAS操作,万一成功了呢!多线程优化
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 判断 数组长度是否达到CPU核数的最大值,控制是否对cells数组进行扩容
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {// 扩容cells数组
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1]; // 左移一位等价于将数组长度 X 2
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue;
}
h = advanceProbe(h); // 递进线程随机数,进行下一轮循环
}
// cells == as == null 初始化cells数组,cellsBusy != 0 表明有其他线程正在操作cells数组
// CAS成功拿到锁,进行初始化操作
// 初始化成功后 将 cellsBusy 复位
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); // 直接操作cell 赋值,此步操作线程安全,提前对cell初始化,
cells = rs;
init = true;
}
} finally {
cellsBusy = 0; // 释放锁
}
if (init) // 初始化完成该线程直接退出
break;
}
// 执行到这里说明线程没有竞争到锁,何不让其尝试对base进行CAS呢,万一成功了呢!减少CPU过多无畏的自旋操作
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
}
}
总结:
对于AtomicLong原子类虽然能保证线程操作的原子性,但是在多线程竞争的情况下同时只会有一个线程能获取到锁进行累加操作,其他CPU只能在那自旋空转(浪费CPU资源)。而对于LongAdder来说,当存在多线程并发操作时,会将其累加操作分散到不同的cell中,增加的了线程并行度(最大限度的压榨CPU,减少CPU空转),进而提升了性能