介绍
LongAddr
是JDK1.8
才有的。其在高并发情况下,相比与AtomicLong
的性能更高。本篇主要分析一下其实现原理。并且与AtomicLong
做一个性能对比测试。
AtomicLong
利用CPU
对CAS
实现的原子化指令实现。
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);// 先从内存中拿到最新值
// 做比较并交换,(内存地址,偏移量,最新值,准备更新的值)
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
以上代码是CAS
算法的源码。有一个do while
的自旋操作。如果并发量过大。会导致自旋的次数过多。那么性能就下来了。大家都在集中抢占成员变量value
。同一时间有上百个线程在抢value
的更新权限。那不得。。。
既然大家争抢同一资源过于凶猛,狼多肉少,那就多来点肉呗。
把value
值分一下,分成比如4份。比如value=4,分成4份之后,每份都是1,[1,1,1,1],这个时候想要累加1,只需要找到4个值中的一个,累加1即可,如果正好有4个线程的话,各加个的,互不打扰。value = 1+1+1+2 = 5。跟我们想要的结果是一致的。这样,原来的竞争压力就被分散,相当于降低到了原来的1/4。相信这个原理不难理解。
来分析一下源代码:
// LongAddr.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);
}
}
上面这个类真的挺晦涩的。我把它愿意调整下。看的舒服些。愿意没有变化。
public void add(long x) {
Cell[] as; // Cell[]对象是用来存储上文说的value拆分之后的数据的对象
long b, v;
int m;
Cell a;
// 如果cells没有初始化并且通过CAS计算base值成功了,就不用拆分了。直接离开
if ((as = cells) == null && casBase(b = base, b + x)) {
return;
}
boolean uncontended = true;
boolean enterAccumulate =
(as == null) // cells是否初始化
|| (m = as.length - 1) < 0// cells数组的长度是否大于1
|| (a = as[getProbe() & m]) == null// 从cells中拿一个值来看看是否为null
|| !(uncontended = a.cas(v = a.value, v + x));// 尝试对cells中的一个值做累加
if (enterAccumulate) {
longAccumulate(x, null, uncontended);
}
}
先说下LongAddr
的设计原则
1.首先使用base
来存储原值,在低并发状态下,优先使用对原值base
做CAS
操作,如果能成功,那么尽量不要使用Cell[]
数组,因为存储也是有代价的。因为LongAddr
的性能代价是用存储换来的。
2.一旦出现了对base
原值的CAS
更新操作失败,说明有竞争了,那么就开始启用Cell[]
,拆分原值。
3.一旦开始使用Cell[]
数组,就回不去了。以后一直都会使用下去。
综合上述原则,解释下上面这段代码(Cell
这个对象自己看下源码就能理解)
1.首先检查cells
是否初始化,如果没有就对base原值做CAS
操作,如果成功则离开(说明没有竞争)
2.如果对base
原值做CAS
操作失败,开始启用Cell[]
3.longAccumulate
方法是用来做cell
数组的初始化和扩容的。有4个条件来判断是否要进入longAccumulate
方法
说下4条件(4个条件只要一个为true
就会进入longAccumulate
)
① as == null
,说明没有初始化呢,那就进去初始化呗
② (m = as.length - 1) < 0 => as.length < 1
,也是说明没有初始化,或者初始化到一半,那就进去初始化呗
③(a = as[getProbe() & m]) == null
,拿一个值出来看看是否为空,说明也要进去初始化一下
④ !(uncontended = a.cas(v = a.value, v + x)
,尝试对数组中的这个cell做累加操作,如果成功就完事,如果失败,说明竞争还是很激烈,那就扩容呗。所以进去扩容下。
下面说说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) {// cells已经初始化
} else if (cellsBusy == 0 && cells == as && casCellsBusy()) {// cells还没有初始化,并且也没有其他线程在做初始化动作,cellsBusy == 0表示未上锁
} else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) {// 有其他线程在做初始化或者扩容动作,这边直接尝试对base原值做累加动作
}
}
}
我把主分支拉出来了,有3个分支。把顺序颠倒下,好理解。
1.if (cellsBusy == 0 && cells == as && casCellsBusy())
cells
还没有初始化,并且也没有其他线程在做初始化动作,cellsBusy == 0
表示未上锁
2.if ((as = cells) != null && (n = as.length) > 0)
cells
已经初始化
3.if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
有其他线程在做初始化或者扩容动作,这边直接尝试对base原值做累加动作
1.看下初始化动作
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];// 先来2个的数组,不够后面再扩容
rs[h & 1] = new Cell(x);// 0,1随便来一个初始化了
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;// 解锁
}
if (init)
break;
2.看下已经初始化之后的动作
if ((a = as[(n - 1) & h]) == null) {// 选出来的那个cell为null
if (cellsBusy == 0) { // 确认cell数组没有在做扩容操作
Cell r = new Cell(x); // new Cell
if (cellsBusy == 0 && casCellsBusy()) {// 加锁准备把cell对象塞进数组
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;// 把刚刚创建的cell对象塞进数组
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))))// 尝试做CAS更新动作,如果失败就继续循环呗
break;
else if (n >= NCPU || cells != as)// cells的容量有上线,一般最多是等于,应该不会超过。写上大于估计是为了以防万一
collide = false; // 不再扩容
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {// 扩容,上锁
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];// 扩容成2倍
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);
最后看下LongAddr
怎么取值
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;
}
这段不难看明白,说白了就是全部累加起来呗。但是在高并发时,它与真实值有一定的差距。把原来的一个值分散成8个或者16个。那么你做sum
操作时,各个线程还在不停修改值。分的越多,误差越大。
看下类图结构
除了LongAddr
还有其他几个类。原理基本是类似的。可以自己看源码。
总结
并发大师的思路真的很牛逼,但是这源码吧,我也不敢说不好,可就是觉得晦涩,看着费劲。
性能测试
为了更形象的提现LongAddr
高性能,我把AtomicLong
拿过来做了一下性能对比测试。
测试工具:OpenJdk
的BenchMark
工具,JMH
系统硬件资源:Mac OS,I7 4核CPU
看下测试代码:
@State(value = Scope.Benchmark)
public class BenchMarkTest {
private static final LongAdder LONG_ADDER_VALUE = new LongAdder();
private static final AtomicLong ATOMIC_LONG_VALUE = new AtomicLong(0);
@Param(value = {"10"})
private int thread;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(BenchMarkTest.class.getSimpleName())
.warmupIterations(3)// 预热3轮
.measurementTime(TimeValue.seconds(1))
.measurementIterations(5)// 度量5轮,总共测试5轮来度量性能
.forks(1)
.threads(10)
.result("result.json")
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opt).run();
}
@Benchmark
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void longAddrIncrementTest() {
for (int i = 0; i < 100000; i++) {
LONG_ADDER_VALUE.increment();
}
}
@Benchmark
@BenchmarkMode({Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void atomicLongIncrementTest() {
for (int i = 0; i < 100000; i++) {
ATOMIC_LONG_VALUE.incrementAndGet();
}
}
}
每次测试预热3轮,度量测试5轮。每轮1秒钟。分别在(1,3,5,10个并发)情况下查看吞吐量和响应时间。
没一轮测试都是把一个long
类型的值从0累加到10万。
因为这个操作是纯粹的CPU
密集型工作,所以线程量没必要上到很高。
下面看测试结果。
红色:AtomicLong
的运行状况
蓝灰色:LongAddr
的运行状况
上图是吞吐量,ops/s
,可以看出随着线程的增长,LongAddr
的优势非常明显。10线程时,相差13倍。
上图是响应时间,单位ms/op
,可以看出随着线程的增长,LongAddr
的优势非常明显。10线程时,相差13倍。