LongAdder类 的累加原理
- JUC中的 LongAdder类 的主要作用是进行基本类型 long 的多线程累加求和操作。
- JUC中的 AtomicLong 类也可以实现线程安全的累加操作,并且这样的 原子类型 还能更丰富的功能,例如 线程安全的自增操作(
incrementAndGet()
)、自定义的计算操作updateAndGet(IntUnaryOperator updateFunction)
等等。
那么为什么JUC还要单独提供LongAddr类呢?
因为LongAdder类在做 累加 这方面是专业的,与AtomicLong 类 相比,LongAdder类的累加性能要高出许多倍。
性能比较
以下用一个多线程并发累加的案例来演示比较 LongAdder类 与 AtomicLong类 的性能:
// 在此处实现了一个通用的比较方法,让 LongAdder对象 和 AtomicLong对象 都使用这同一个方法的流程,保证公平性。
// Supplier接口 和 Consumer接口 都是 函数式接口(即有且仅有一个抽象方法,但是可以有多个非抽象方法的接口)
// Supplier接口的抽象方法可作为提供者,不传入参数,但返回结果;
// Consumer接口的抽象方法可作为消费者,传入一个参数,但不返回结果。
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 4; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}, "t_" + i));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach((t) -> {
try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); }
});
LOGGER.info("累加结果: {}, 花费时间:{} ms", adder, (System.nanoTime() - start)/1000000);
}
// 使用
for (int i = 0; i < 5; i++) {
demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
LOGGER.info("==============================");
for (int i = 0; i < 5; i++) {
demo(() -> new LongAdder(), adder -> adder.increment());
}
结果:
累加结果: 2000000, 花费时间:49 ms
累加结果: 2000000, 花费时间:47 ms
累加结果: 2000000, 花费时间:42 ms
累加结果: 2000000, 花费时间:42 ms
累加结果: 2000000, 花费时间:33 ms
=============================
累加结果: 2000000, 花费时间:17 ms
累加结果: 2000000, 花费时间:6 ms
累加结果: 2000000, 花费时间:7 ms
累加结果: 2000000, 花费时间:7 ms
累加结果: 2000000, 花费时间:6 ms
从结果可以看出,两种累加方法的结果都是正确的,确实没有线程不安全问题,但是很明显,LongAdder类的累加性能更好。
工作原理
那么 LongAdder类 内部是如何优化,来让其累加性能如此优异的呢?
LongAdder类的重要变量:
// 累加单元数组, 因 Cell 类是静态内部类,所以自带 Load On Demand(懒加载)功能
// 且Cell 类上有@sun.misc.Contended,防止了缓存行的伪共享
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 将值累加这个域
transient volatile long base;
// 此变量时CAS锁的关键参数。值为0, 表示无锁,值为1, 表示有锁。
transient volatile int cellsBusy;
几个常用方法的功能概括:
int getProbe()方法,用来获得线程对应的probe值,类似于hash值,是一种hash函数映射。
int advanceProbe(int probe)方法:用来重新生成一个probe值,为什么要重新生成呢?因为在之前的probe映射到的cell中累加,一直失败。
boolean casCellsBusy()方法:用来上cas锁 ——— 上锁成功则返回true,此时cellsBusy应为1;上锁失败则返回false,证明有其他线程正在使用锁;使用锁结束后,应该让cellsBusy = 0,变成无锁状态。
简单来说,就是 ”映射归约,分而治之”,类似于 MapReduce :
-
当cells还没有初始化,且线程没有竞争时,直接用
boolean casBase(b = base, b + x)
将累加结果加到 base 上,累加成功则返回 true。-
if ((as = cells) != null || !casBase(b = base, b + x)) { // 此代码块用来操作 cell[] ,可进行初始化,扩容,和线程映射到某个 cell 中 boolean uncontended = true; 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); // 核心方法 }
-
-
在有竞争时,创建1个累加单元数组 cell[],内部包含多个累加单元 cell;
-
这样,可以将不同的线程**映射到不同的 cell **上进行累加;
-
最后将 base 和 cell[] 中每个 cell 的结果汇总,就是最终结果。
-
这样它们在累加时操作的不同的 cell 变量,因此减少了 CAS 重试失败 而导致的空转及性能损耗。
cells 的扩容步骤
Cell[] rs = new Cell[n << 1]; // 先 new 一个双倍容量的cell数组
for (int i = 0; i < n; ++i)
rs[i] = as[i]; // 再从旧 cell[] 中将 cell 依次复制到新cell[]中。
cells = rs; // 修改成员变量的引用,让旧cell[]能被GC。
CAS锁
用CAS实现锁的一种方法:
public class LockCas {
// state = 0代表无锁,state = 1代表有锁。
private AtomicInteger state = new AtomicInteger(0); // 原子类型,线程安全
// 上锁
public void lock() {
while (true) {
if (state.compareAndSet(0, 1)) break;
}
}
// 解锁
public void unlock() {
log.debug("unlock...");
state.set(0);
}
}
附上核心方法 longAccumulate 的源码:
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
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) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
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;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
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
}
h = advanceProbe(h);
}
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;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}