分析
LongAdder类主要的目的是看看其背后的实现是如何?为什么会比我们AtomicLong同志的效率要高?为什么需要LongAdder?其背后的思想是什么?
震惊!!! 开局两张图, 内容全靠编
从简单的入门起手分析源码
@Test
public void test() throws Exception {
LongAdder longAdder = new LongAdder();
longAdder.increment();
System.out.println(longAdder.sum());
}
源码分析开始
现在我们单步调试分析下
LongAdder longAdder = new LongAdder(); 这行是构造方法,但是我发现其实他就初始化了 base(其实没初始化直接默认赋值 0 ) 比较重要还有一个
static final int NCPU = Runtime.getRuntime().availableProcessors();获取 cpu核心 的数量
底层源码是这样的
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null ||
!(uncontended = c.cas(v = c.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
!casBase(b = base, b + x) // 先试试使用 base cas 增加看下是否能够成功, 如果成功了后面就不需要搞那么复杂了
cs == null ----> 如果 cell 没有创建
(m = cs.length - 1) < 0 ----> cell != null 但是 cell 的长度不对
(c = cs[getProbe() & m]) == null ----> 计算出来的 cell 位置为空
!(uncontended = c.cas(v = c.value, v + x)) ----> 如果计算获取出来的 cell 数组中的元素不为空, 则尝试把 cell 数组中的那个元素 +x, 如果上面的条件都成立, 则会执行
longAccumulate(x, null, uncontended); 函数, 这个函数过于复杂(太难了)
分析下面这段代码很复杂, 而且还存在高并发方面的问题需要实时关注,否则光光看肯定是不行的
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// 下面这段代码, 初始化了 h 和 wasUncontended = true
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
// ==============================================================
boolean collide = false; // True if last slot nonempty, 如果为 true 表示最后一个槽(数组的元素位置)不是空的
done: for (;;) {
Cell[] cs; Cell c; int n; long v;
// 如果 cells 不为空, 并且 cells 的长度大于 0 表示 cell 在这里已经做了初始化了, 并且已经创建了 cell 数组
if ((cs = cells) != null && (n = cs.length) > 0) {
// 代码能进入这里说明了, cells 数组初始化完毕, 里面已经存在数据的元素了, 只不过这个元素是否有效就不知道了, 所以需要判断下
if ((c = cs[(n - 1) & h]) == null) {
// 如果这个变量不等于 0 表示已经进入到后面的步骤了, 此时可能存在两个线程, 一个线程已经把cellsBusy设置成了 1 , 所以为了防止这种情况, 做下判断
if (cellsBusy == 0) { // Try to attach new Cell
// 新建已经 cell
Cell r = new Cell(x); // Optimistically create
// 再次做下 cellsBusy 的判断,并且将其置为 1 , 表示现阶段 cas 功能正在执行中, 其他 cas 操作暂时不支持
if (cellsBusy == 0 && casCellsBusy()) {
try { // Recheck under lock
Cell[] rs; int m, j;
// 下面这段代码就承上启下了, 类似于 单例的 两次检测 再次检测下, 是否满足条件
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
// 把新的 cell 赋值到 cells 中
rs[j] = r;
// 回到前面的 for 死循环
break done;
}
} finally {
// 释放锁
cellsBusy = 0;
}
// 这段 if 语句主要是 cells 不能为null,并且 cells的长度已经被确认, 但是里面的值为空, 必须初始化的情况下
continue; // Slot is now non-empty 槽位不是空的, 所以 continue 掉了
}
}
// 未发生碰撞, 最后一个槽不是空的
collide = false;
}
// 说明还不存在竞争关系
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 如果读出来的 cells 其中一个 slot 不为空,则把那个 slot 的值读取出来进行 cas 增加
else if (c.cas(v = c.value,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break;
// 下面的几个小 if , 能到这里的 cells 中一个 slot 不为空,并且前面对 slot 的 cas 失败了
// 如果 cells 数组的长度大于等于 cpu 核心的数量 或者 cells 被其他函数修改了, 和 cs 不相等 cs 的值过时了
else if (n >= NCPU || cells != cs)
collide = false; // At max size or stale
else if (!collide) // 还能怎么办, cas 失败存在碰撞的情况
collide = true;
// 上 cell 忙锁
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 防止 cs 的值过时
if (cells == cs) // Expand table unless stale
// 扩容 2 倍
cells = Arrays.copyOf(cs, n << 1);
} finally {
// 释放cell 忙锁
cellsBusy = 0;
}
// 释放碰撞状态
collide = false;
continue; // Retry with expanded table
}
// 再获取新的 hash 值
h = advanceProbe(h);
}
// 如果 cellsBusy 等于空;
// cells 等于 cs (cell 在前面将自己的应用丢给cs后可能被其他函数修改, 所以需要这个判断, 保证cs还是最新的);
// casCellsBusy 尝试修改 cellsBusy 为 1 如果成功说明不存在 冲突, 可以进行修改
else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
try { // Initialize table
/// 防止 cs 过期了
if (cells == cs) {
// new 出 cells 数组
Cell[] rs = new Cell[2];
// 设置 cells 数组中的一个 slot
rs[h & 1] = new Cell(x);
// 把值给 cells
cells = rs;
// 回到 for 死循环
break done;
}
} finally {
// 释放 cells 忙锁
cellsBusy = 0;
}
}
// Fall back on using base
// 如果无法命中前面的 if 则再次尝试修改 base 的值, 如果还是存在竞争则失败
else if (casBase(v = base,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break done;
}
}
至此我们分析完毕了 LongAdder.add 源码的分析
现在看下 LongAdder.sum 源码
public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
这可真是太喜欢了. base + cell[0] + ... + cell[cell.length - 1]
总结下, 要不然有点乱
首先如果能够直接在 base 上累加那就在 base 上累加, 如果不能再 base 上累加, 那就看看 cells 数组是否被初始化, 以及里面的元素是否全部被初始化, 和扩容问题等等
-
如果
cells未被初始化, 则借助cellsBusy进行cas上锁, 然后new出 一个rs数组将数组赋值给cells完成第一次的初始化, 这次初始化会将length确定, 并且数组内部一个slot已经被填充 -
如果
cells被初始化, 则判断下length是否确定已经他的slot是否已经被填充, 如果length已经确定 则在确认下slot是否有空的, 如果存在空的, 直接new一个 新的cell给他填充, 填充过程为了防止多线程安全问题, 还是使用了我们的cellsBusy做cas锁保证线程安全( 当然是否真的new一个新的还是在另外的已经填充的cell上计算应该需要看hash的计算, 而计算方法在[1]这里当然这个还和线程的探测值有点关系, 没细讲)
[1]hash计算过程
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
THREAD_PROBE.set(Thread.currentThread(), probe);
return probe;
}
-
如果
cells已经被slot槽已经填充完毕我们还可以扩容新的slot只要不满足这个条件就好else if (n >= NCPU || cells != cs)还是借助了cellsBusy做cas锁, 然后n << 1的扩容cells = Arrays.copyOf(cs, n << 1);(两倍扩容) -
在看那段源码你会发现他在最后一个 if 语句中还是做了一次对 base 的 cas
else if (casBase(v = base, (fn == null) ? v + x : fn.applyAsLong(v, x)))
LongAdder 的优点
现在呢, 这样做的好处在哪?
其实很简单, cas 的有点很多, 无锁效率高, 内核把关, 但是他只能做的操作实在太少, 最致命的是如果遇到特别大的高并发你会发现它很多时候再做这样的判断
while (true) {
int oldVal = memoryVal;
if (U.cas(oldVal, newVal)) { // 如果 cas 成功, 隐藏的做了个 memoryVal = newVal
break;
}
}
如果 cas 成功, 则直接跳出循环, 但是如果存在一大堆线程, 比如 10000 个, 那么只存在一个线程会完成 cas, 其他的线程会发现不满足条件, 再次获取主存中的 value 赋值给 oldVal 再做判断, 实际上会出现无数的死循环, 贼消耗 cpu核心 资源
所以 LongAdder 这种将 value 拆分成 base + cells[n].sum 的方式就此出现, 线程不再盯着一个 value 做 cas 了, 即使线程1 在 cell[0] 做了 cas 将 cell[0] 从 1 变成 2 , 线程2 把 cell[1] 从 0 cas 成了 1 , 在这个过程中我们建立了多个 茅坑(cell), 这样在抢占的时候就多了个机会, 即便最后你需要把屎拿出来称重, 只要把 茅坑1 和 茅坑2 的屎 加上 你自己 还没拉出来的一起称一称重量就行, 好处多多
LongAdder 缺点
但是坏处呢?
也是存在的, 就是 sum 的结果在某一个时刻可能是不准确的, 因为无法保证在计算 最终值的过程中茅坑中是否还会有人在拉屎, 还会不会有新的人在拉屎, 你不知道, 但直到最后, 你把茅坑炸了, 不让人拉屎了, 这个时候你称的重量才是最终总量(最终一致性)
注意开始着手写一个只记得
LongAdder时, 我推荐先不急, 为了不让博客里面的图片很多, 我省略了对每个类的特写, 这步不能丢, 推荐看看Cell类, 你会发现@jdk.internal.vm.annotation.Contended注解, 其作用很明显, 我举个简单的梨子, 你房子是老旧房需要拆迁(缓存行A中的一个变量), 但你的房屋临近的还有另一栋新盖的房子(缓存行A的另一个变量, 实际一个变量不一定只占用一个byte, 还有注意下是同一个缓存行哦!), 你找到了一个足以拆掉附近64栋房子的炸药(缓存行大小64byte), 直接给他炸了, 你只能赔钱(无端端把人家的房子炸了你需要花费更多的时间叫他自己再建房子, 钱你付), 现在使用了Contended, 说明你有钱了, 把周围 64 byte的地全卖了, 除了中间你的房子, 其余都种了菜, 这样新的房子只能选择在这 64 byte之外的地方(另一个缓存行) 这样代价很大, 但在高并发环境下效率提高显著----------------- 对了, 我是 jdk 11 其他版本的 jdk 可能注解名字一样但包位置不同(可能)
自己动手写一个简单的 LongAdderBug ?(复习时刻)
Cell 类的创建过程分析
字段
我们知道他把我们的 value 分割成 base 和 cells , 目前我们缺少了 Cells 类, 这个类的字段想起来应该很简单, 只要储存一个 value , 这个 value 为了防止多线程安全需要加上 volatile 并且这个类不需要对外公开? 对吧? 所以内部类走起
private volatile long value
方法
那么现在考虑他有哪些方法呢?
对一个字段的方法无非就几种, 读和写
- 首先构造函数一个, 用于设置值给
value
public Cell(long value) {
this.value = value;
}
- 发现这个值需要做
add和sub但实际上这两个方法可以统一成set, 但是他有可能是多线程操作的, 容易出现线程安全问题, 需要做cas操作进行, 同时需要返回值, 判断是否cas成功了
既然需要 cas 那么便可以使用 MethodHandlers 类进行操作, 为了防止别人以为我抄了代码, 这里我选择使用 Unsafe 类进行 cas 操作
听说
jdk11的Unsafe类可以直接getUnsafe方法获取, 但这里给给出了反射方式获取的代码吧, 虽然这段代码也很简单
现在我们考虑写的两个方法, 一个是写方法 compareAndSet 和另一个 getValue 方法
// @jdk.internal.vm.annotation.Contended // 这里需要特殊的方法导入, 现在我没做等下再做
private static class Cell {
private volatile long value;
public Cell(long value) {
this.value = value;
}
public boolean compareAndSet(long expect, long newValue) {
long fieldOffset = 0;
try {
fieldOffset = unsafe.objectFieldOffset(this.getClass().getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return unsafe.compareAndSwapLong(this, fieldOffset, expect, newValue);
}
public long getValue() {
return this.value;
}
private static final Unsafe unsafe;
static {
// unsafe = Unsafe.getUnsafe();
Class<Unsafe> unsafeClass = Unsafe.class;
try {
Field declaredField = unsafeClass.getDeclaredField("theUnsafe");
declaredField.setAccessible(true);
unsafe = (Unsafe) declaredField.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
LongAdderBug类的源码编写
字段
那我现在我们来想想新的类 LongAdderBug 方法需要哪些字段
base 逃不掉了, cells 也是, 还需要啥呢? 哦 还有一个 cas 的锁, 这里跟前面分析的一样使用 cellsBusy 做锁
这里我就不做 Unsafe 方法了, 太麻烦使用 MethodHandlers 获取吧
private volatile long base;
private volatile Cell[] cells;
private volatile boolean cellBusy;
private static final VarHandle BASE;
private static final VarHandle CELLBUSY;
public LongAdderBug() {
}
static {
MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
BASE = lookup.findVarHandle(LongAdderBug.class, "base", long.class);
CELLBUSY = lookup.findVarHandle(LongAdderBug.class, "cellBusy", boolean.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
方法
在方法方面的考虑这个函数我就不考虑什么 reset 之类的东西直接上 自增 自减 还有加多少 统计下当前值是多少 就这几个方法吧, 简单点
而核心方法 add 思想其实很简单, 能使用 base 进行计算就直接使用它, 如果不能出现了 cas 冲突, 则去找 cells 多个数组进行 cas 计算, 等到合适的时候再去统计就好了, 如果 cell slot 为空, 则直接 new 一个新的 slot , 将 val 值传入构造函数的方法中
下面是全代码: 注意下 Contended 你可能要另外配置, 我 openJDK11 的包是 @jdk.internal.vm.annotation.Contended 这里 但是 jdk 1.8 好像不是, 你需要修改, 然后再添加一些虚拟机参数否则无法开启这个功能, jvm 默认关闭的
public class MyLongAdder {
private volatile long base;
private final Cell[] cells = new Cell[Runtime.getRuntime().availableProcessors() + 1];
private volatile boolean cellBusy = false;
private static final VarHandle BASE;
private static final VarHandle CELLBUSY;
public static void main(String[] args) throws Exception {
ArrayList<MyLongAdder> bugs = new ArrayList<>();
ExecutorService threadPool = Executors.newFixedThreadPool(100);
int count = 10000;
for (int j = 0; j < 1000; j++) {
MyLongAdder adderBug = new MyLongAdder();
CountDownLatch latch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
threadPool.submit(() -> {
adderBug.increment();
latch.countDown();
});
}
latch.await();
bugs.add(adderBug);
}
threadPool.shutdown();
TimeUnit.SECONDS.sleep(10);
bugs.stream().filter(myLongAdder -> {
if (count == myLongAdder.sum()) {
return false;
}
return true;
}).forEach(myLongAdder -> System.out.println(myLongAdder.sum()));
System.err.println("没有打印, 就代表着成功!!!");
}
public MyLongAdder() {
}
public long sum() {
long sum = base;
for (Cell cell : this.cells) {
if (null != cell) {
sum += cell.value;
}
}
return sum;
}
public void add(long val) {
long b = base;
synchronized (this) {
if (casBase(b, b + val)) {
return;
}
for (; ; ) {
Cell[] cs = this.cells;
int index = Math.abs(ThreadLocalRandom.current().nextInt()) % cs.length;
long cv;
if (casBase(cv = base, cv + val)) {
break;
}
else if (cs[index] == null) {
if (!cellBusy && casCellsBusy()) {
try {
Cell r = new Cell(val);
Cell[] rs = this.cells;
if (rs[index] == null) {
rs[index] = r;
}
} finally {
cellBusy = false;
}
}
}
else if (cs[index].compareAndSet(cv = cs[index].value, cv + val)) {
break;
}
}
}
}
private boolean casCellsBusy() {
return CELLBUSY.compareAndSet(this, false, true);
}
private boolean casBase(long expect, long val) {
return BASE.compareAndSet(this, expect, val);
}
private boolean casBase(long val) {
return (boolean) BASE.getAndAdd(this, val);
}
public void increment() {
add(1L);
}
public void decremenet() {
add(-1L);
}
static {
MethodHandles.Lookup lookup = MethodHandles.lookup();
try {
BASE = lookup.findVarHandle(MyLongAdder.class, "base", long.class);
CELLBUSY = lookup.findVarHandle(MyLongAdder.class, "cellBusy", boolean.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
// TODO 这里需要做缓存行填充, 提高高并发效率, 但是需要特殊开启方法, 我使用的是 idea 按下快捷键就直接添加了 汗~~~
@jdk.internal.vm.annotation.Contended
private static class Cell {
private volatile long value;
public Cell(long value) {
this.value = value;
}
public boolean compareAndSet(long expect, long newValue) {
long fieldOffset = 0;
try {
fieldOffset = unsafe.objectFieldOffset(this.getClass().getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return unsafe.compareAndSwapLong(this, fieldOffset, expect, newValue);
}
public long getValue() {
return this.value;
}
private static final Unsafe unsafe;
static {
unsafe = Unsafe.getUnsafe();
}
@Override
public String toString() {
return "Cell{" + "value=" + value + '}';
}
}
}