问题
(1)java8中为什么要新增LongAdder?
(2)LongAdder的实现方式?
(3)LongAdder与AtomicLong的对比?
简介
LongAdder是java8中新增的原子类,在多线程环境中,它比AtomicLong性能要高出不少,特别是写多的场景。
它是怎么实现的呢?让我们一起来学习吧。
原理
LongAdder的原理是,在最初无竞争时,只更新base的值,当有多线程竞争时通过分段的思想,让不同的线程更新不同的段,最后把这些段相加就得到了完整的LongAdder存储的值。
源码分析
LongAdder继承自Striped64抽象类,Striped64中定义了Cell内部类和各重要属性。
主要内部类
1 | // Striped64中的内部类,使用@sun.misc.Contended注解,说明里面的值消除伪共享 |
Cell类使用@sun.misc.Contended注解,说明是要避免伪共享的。
使用Unsafe的CAS更新value的值,其中value的值使用volatile修饰,保证可见性。
关于Unsafe的介绍请查看【死磕 java魔法类之Unsafe解析】。
关于伪共享的介绍请查看【杂谈 什么是伪共享(false sharing)?】。
主要属性
1 | // 这三个属性都在Striped64中 |
最初无竞争或有其它线程在创建cells数组时使用base更新值,有过竞争时使用cells更新值。
最初无竞争是指一开始没有线程之间的竞争,但也有可能是多线程在操作,只是这些线程没有同时去更新base的值。
有过竞争是指只要出现过竞争不管后面有没有竞争都使用cells更新值,规则是不同的线程hash到不同的cell上去更新,减少竞争。
add(x)方法
add(x)方法是LongAdder的主要方法,使用它可以使LongAdder中存储的值增加x,x可为正可为负。
1 | public void add(long x) { |
(1)最初无竞争时只更新base;
(2)直到更新base失败时,创建cells数组;
(3)当多个线程竞争同一个Cell比较激烈时,可能要扩容;
longAccumulate()方法
1 | final void longAccumulate(long x, LongBinaryOperator fn, |
(1)如果cells数组未初始化,当前线程会尝试占有cellsBusy锁并创建cells数组;
(2)如果当前线程尝试创建cells数组时,发现有其它线程已经在创建了,就尝试更新base,如果成功就返回;
(3)通过线程的probe值找到当前线程应该更新cells数组中的哪个Cell;
(4)如果当前线程所在的Cell未初始化,就占有占有cellsBusy锁并在相应的位置创建一个Cell;
(5)尝试CAS更新当前线程所在的Cell,如果成功就返回,如果失败说明出现冲突;
(5)当前线程更新Cell失败后并不是立即扩容,而是尝试更新probe值后再重试一次;
(6)如果在重试的时候还是更新失败,就扩容;
(7)扩容时当前线程占有cellsBusy锁,并把数组容量扩大到两倍,再迁移原cells数组中元素到新数组中;
(8)cellsBusy在创建cells数组、创建Cell、扩容cells数组三个地方用到;
sum()方法
sum()方法是获取LongAdder中真正存储的值的大小,通过把base和所有段相加得到。
1 | public long sum() { |
可以看到sum()方法是把base和所有段的值相加得到,那么,这里有一个问题,如果前面已经累加到sum上的Cell的value有修改,不是就没法计算到了么?
答案确实如此,所以LongAdder可以说不是强一致性的,它是最终一致性的。
LongAdder VS AtomicLong
直接上代码:
1 | public class LongAdderVSAtomicLongTest { |
运行结果如下:
1 | threadCount:1, times:10000000 |
可以看到当只有一个线程的时候,AtomicLong反而性能更高,随着线程越来越多,AtomicLong的性能急剧下降,而LongAdder的性能影响很小。
总结
(1)LongAdder通过base和cells数组来存储值;
(2)不同的线程会hash到不同的cell上去更新,减少了竞争;
(3)LongAdder的性能非常高,最终会达到一种无竞争的状态;
彩蛋
在longAccumulate()方法中有个条件是n >= NCPU
就不会走到扩容逻辑了,而n是2的倍数,那是不是代表cells数组最大只能达到大于等于NCPU的最小2次方?
答案是明确的。因为同一个CPU核心同时只会运行一个线程,而更新失败了说明有两个不同的核心更新了同一个Cell,这时会重新设置更新失败的那个线程的probe值,这样下一次它所在的Cell很大概率会发生改变,如果运行的时间足够长,最终会出现同一个核心的所有线程都会hash到同一个Cell(大概率,但不一定全在一个Cell上)上去更新,所以,这里cells数组中长度并不需要太长,达到CPU核心数足够了。
比如,笔者的电脑是8核的,所以这里cells的数组最大只会到8,达到8就不会扩容了。