Java 8新增了一个新的类LongAdder,以空间换时间的方式提高高并发场景下CAS操作的性能。
LongAdder的核心思想是热点分离,与ConcurrentHashMap中的分段锁非常类似。LongAdder将常规的value属性拆成一个数组;并发访问时,通过哈希算法将线程映射到对应的元素进行CAS操作;访问value时,再进行求和计算。
测试用例:
public class LongAdderVsAtomicLong {
public static void main(String[] args) {
testAtomicLongVSLongAdder(1, 10000000);
testAtomicLongVSLongAdder(10, 10000000);
testAtomicLongVSLongAdder(20, 10000000);
}
public static void testAtomicLongVSLongAdder(final int threadCount, final int times) {
try {
System.out.println("threadCount = " + threadCount + ",times = " + times);
long start = System.currentTimeMillis();
testLongAdder(threadCount, times);
System.out.println("LongAdder elapse:" + (System.currentTimeMillis() - start) + "ms");
long start2 = System.currentTimeMillis();
testAtomicLong(threadCount, times);
System.out.println("AtomicLong elapse:" + (System.currentTimeMillis() - start2) + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void testAtomicLong(int threadCount, int times) throws InterruptedException {
AtomicLong atomicLong = new AtomicLong();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
list.add(new Thread(() -> {
for (int j = 0; j < times; j++) {
atomicLong.incrementAndGet();
}
}));
}
for (Thread t : list) {
t.start();
}
for (Thread t : list) {
t.join();
}
}
private static void testLongAdder(int threadCount, int times) throws InterruptedException {
LongAdder longAdder = new LongAdder();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
list.add(new Thread(() -> {
for (int j = 0; j < times; j++) {
longAdder.increment();
}
}));
}
for (Thread t : list) {
t.start();
}
for (Thread t : list) {
t.join();
}
}
}
输出结果:
threadCount = 1,times = 10000000
LongAdder elapse:136ms
AtomicLong elapse:53ms
threadCount = 10,times = 10000000
LongAdder elapse:183ms
AtomicLong elapse:1821ms
threadCount = 20,times = 10000000
LongAdder elapse:440ms
AtomicLong elapse:3372ms
LongAdder的核心思想是热点分离,与ConcurrentHashMap中的分段锁非常类似。LongAdder将常规的value属性拆成一个数组;并发访问时,通过哈希算法将线程映射到对应的元素进行CAS操作;访问value时,再进行求和计算。
AtomicLong使用内部属性value保持这实际的long值,所有的操作都是针对该value属性进行的。在高并发环境下,value变量是大量线程竞争的一个热点。重试线程越多,就意味着CAS的失败概率越高,从而浪费大量CPU性能。
LongAdder继承自Striped64, 其中有三个非常重要的属性,如下代码所示:
/**
* cells的哈希表. 非空时,长度为2的幂。
*/
transient volatile Cell[] cells;
/**
* 基本值,主要在没有争用时使用,但也用作在哈希表初始化竞争中的一种应变计划。通过CAS更新。
*/
transient volatile long base;
/**
* 当resize和或创建Cells被使用的自旋锁 (通过CAS实现)。
*/
transient volatile int cellsBusy;
如果没有发生线程竞争的情况,LongAdder会将要累加的数会通过CAS累加到base中;如果发生线程竞争的情况,会将要累加的数累加到cells数组中的某个Cell元素中。所以LongAdder的sum()方法的实现也必定是将base和cells中的值进行累加,这一点可以通过以下源码得到验证。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
LongAdder的base类似于AtomicInteger里面的value,在没有竞争的情况下,cells数组为null,这时只使用base进行累加;而一旦发生竞争,cells数组就进行初始化。
cells数组第一次初始化长度为2,以后每次扩容都变为原来的两倍,一直到cells数组的长度大于等于当前服务器CPU的核数。同一时刻能持有CPU时间片去并发操作同一个内存地址的最大线程数最多也就是CPU的核数。
刚才有说到“如果发生线程竞争的情况,会将要累加的数累加到cells数组中的某个Cell元素中。”,在add方法中,会通过“as[Thread.currentThread().threadLocalRandomProbe & (as.length - 1)])”即"as[getProbe() & m])”,将当前线程的threadLocalRandomProbe属性和cells的长度进行与运算来计算出当前线程对应的cell对象的索引。
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
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);
}
}
在JDK中,Striped64除了LongAdder以外,还有三个子类。这三个子类分别是java.util.concurrent.atomic.LongAccumulator、java.util.concurrent.atomic.DoubleAdder和java.util.concurrent.atomic.DoubleAccumulator。由于底层实现差不多,此处不再赘述了,感兴趣的同学可以自我学习。