Longadder和atomiclong的区别?
✅LongAdder和AtomicLong的区别?
典型回答
LongAdder是Java 8中推出了一个新的类,主要是为了解决AtomicLong在多线程竞争激烈的情况下性能并不高的问题。它主要是采用分段+CAS的方式来提升原子操作的性能。
相比于AtomicLong,LongAdder有更好的性能,但是LongAdder是典型的以空间换时间的实现方式,所以他所需要用到的空间更大, 而且LongAdder可能存在结果不准确的问题,而AtomicLong并不会。
扩展知识
AtomicLong实现原理
AtomicLong和AtomicInteger、AtomicDouble 等其他原子操作的类一样,都是基于Unsafe 实现的。Unsafe 是一个用来进行硬件级别的原子操作的工具类。在AtomicLong 中定义如下:
private static final Unsafe unsafe = Unsafe.getUnsafe();
并且通过在类中定义一个volatile的变量用来计数:
private volatile long value;
以下几个常用的方法具体实现如下:
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
public final long addAndGet(long delta) {
return unsafe.getAndAddLong(this, valueOffset, delta) + delta;
}
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
public final long getAndSet(long newValue) {
return unsafe.getAndSetLong(this, valueOffset, newValue);
}
可以看到,都是基于unsafe来实现的,其实就是调用了底层的CAS操作来进行原子操作的。
LongAdder实现原理
JDK 1.8中的LongAdder继承自抽象类java.util.concurrent.atomic.Striped64 的,这个类也是JDK 1.8中新增的,他主要就是用来在给并发场景中提供计数支持的。
Striped64的设计思路和ConcurrentHashMap类似,都是希望通过分散竞争的方式来提升并发的性能。再具体是线上,主要依赖了其中的以下两个字段:
/**
* Table of cells. When non-null, size is a power of 2.
*/
transient volatile Cell[] cells;
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
transient volatile long base;
通过查看LongAdder中的add方法的代码,我们其实就能很容易的理解他的实现细节:
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);
}
}
首先就是先尝试通过CAS更新计数器base的值,如果在竞争不激烈的情况下,是可以直接更新成功的,如果直接成功那就和AtomicLong一样了。
但是如果更新失败,那么则是认为当前的并发竞争比较激烈,那么就会尝试通过cells数组来分散计数
Striped64根据线程来计算哈希,然后将不同的线程分散到不同的Cell数组的index上,然后这个线程的计数内容就会保存在该Cell的位置上面,基于这种设计,最后的总计数需要结合base以及散落在Cell数组中的计数内容。
是不是看上去和ConcurrentHashMap的分段锁很像。
当需要进行统计数量的时候,则需要结合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为什么可能会不准确
通过查看LongAdder的代码了解了他的原理之后,很容易可以发现一个问题,那就是其实LongAdder的累加结果可能是不准的。没错,其实在JDK的源码中也明确的说了:
Returns the current sum. The returned value is NOT an atomic snapshot; invocation in the absence of concurrent updates returns an accurate result, but concurrent updates that occur while the sum is being calculated might not be incorporated.
以上,就是LongAdder的sum方法的说明,其实就是:在没有并发更新的情况下sum方法将返回准确的结果,但是在计算总和时发生的并发更新可能不会被计算进来,所以就有可能会不准确。
DoubleAdder
除了LongAdder以外,JDK 1.8中还提供了针对浮点数进行原子操作的DoubleAdder,他也是基于Striped64实现的,原理和LongAdder并无差别。
适用场景
相比于AtomicLong,LongAdder性能更好,但是有一个小缺点就是有可能返回值没那么准确,但是也只是在并发极高,刚好返回sum时候有其他原子操作在进行累加的时候才会出现。
基于以上的特性,LongAdder比较适合于并发竞争激烈,但是对数据准确度要求并不是百分之百准确的场景,比如微博点赞、文章阅读量的统计等等场景中。
##