03_LongAdder 源码分析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了03_LongAdder 源码分析相关的知识,希望对你有一定的参考价值。
03_LongAdder 源码分析
- AtomicLong 和 LongAdder对比
- AtomicLong
- LongAdder
- LongAdder和AtomicLong性能测试
- LongAdder为什么这么快
- 1、 设计思想上,LongAdder采用**"分段"的方式降低CAS失败的频次**
- 2、**使用Contended注解来消除伪共享**
- **3、惰性求值**
- LongAdder源码剖析
- AtomicLong可以弃用了吗?
- 总结
上文中我们分析了:CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。(不停的自旋,进入一个无限重复的循环中)
那我们今天分析下LongAdder这个类
AtomicLong 和 LongAdder对比
最近阿里巴巴发布了Java开发手册(泰山版) (公众号回复: 开发手册 可收到阿里巴巴开发手册(泰山版 2020.4.22发布).pdf),其中第17条写到:
对于Java项目中计数统计的一些需求,如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)
在大多数项目及开源组件中,计数统计使用最多的仍然还是AtomicLong,虽然是阿里巴巴这样说,但是我们仍然要根据使用场景来决定是否使用LongAdder。
今天主要是来讲讲LongAdder的实现原理,还是老方式,通过图文一步步解开LongAdder神秘的面纱,通过此篇文章你会了解到:
- 为什么AtomicLong在高并发场景下性能急剧下降?
- LongAdder为什么快?
- LongAdder实现原理(图文分析)
- AtomicLong是否可以被遗弃或替换?
本文代码全部基于JDK 1.8,建议边看文章边看源码更加利于消化
AtomicLong
上文分析cas中我们知道AtomicXX 都是利用cas原理实现的,这里再单独分析下AtomicLong源码
当我们在进行计数统计的时,通常会使用AtomicLong来实现。AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。
AtomicLong实现原理
说到线程安全的计数统计工具类,肯定少不了Atomic下的几个原子类。AtomicLong就是juc包下重要的原子类,在并发情况下可以对长整形类型数据进行原子操作,保证并发情况下数据的安全性。
public class AtomicLong extends Number implements java.io.Serializable
public final long incrementAndGet()
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
public final long decrementAndGet()
return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
我们在计数的过程中,一般使用incrementAndGet()和decrementAndGet()进行加一和减一操作,这里调用了Unsafe类中的getAndAddLong()方法进行操作。
接着看看unsafe.getAndAddLong()方法:
public final class Unsafe
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;
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这里直接进行CAS+自旋操作更新AtomicLong中的value值,进而保证value值的原子性更新。
AtomicLong瓶颈分析
如上代码所示,我们在使用CAS + 自旋的过程中,在高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的自旋会成为瓶颈。
如上图所示,高并发场景下AtomicLong性能会急剧下降,我们后面也会举例说明。
那么高并发下计数的需求有没有更好的替代方案呢?在JDK8 中 Doug Lea大神 新写了一个LongAdder来解决此问题,我们后面来看LongAdder是如何优化的。
LongAdder
LongAdder和AtomicLong性能测试
我们说了很多LongAdder上性能优于AtomicLong,到底是不是呢?一切还是以代码说话:
public class testAtomicLongAdder
public static void main(String[] args) throws InterruptedException
testAtomicLongAdder(1,10000000);
testAtomicLongAdder(10,10000000);
testAtomicLongAdder(100,10000000);
public static void testAtomicLongAdder(int threads, int times) throws InterruptedException
System.out.println("线程数:"+ threads+ "; 累加次数: " + times);
long startAtomic = System.currentTimeMillis();
testTtomicLong(threads,times);
long endTomic = System.currentTimeMillis();
long startLongadder = System.currentTimeMillis();
testLongAdder(threads,times);
long endLongadder = System.currentTimeMillis();
System.out.println("AtomicLong 耗时:" + (endTomic-startAtomic) + "\\n LongAdder 耗时: " + (endLongadder-startLongadder));
System.out.println("---------------------");
private static void testTtomicLong(int threads, int times) throws InterruptedException
AtomicLong atomicLong = new AtomicLong(0);
List<Thread> list = new ArrayList<>();
for (int i = 1; i <=threads; i++)
list.add(new Thread(()->
for (int j = 0; j < times; j++)
atomicLong.incrementAndGet();
));
for (Thread thread : list)
thread.start();
for (Thread thread : list)
thread.join();
System.out.println("AtomicLong value is : " + atomicLong.get());
private static void testLongAdder(int threads, int times) throws InterruptedException
LongAdder longAdder = new LongAdder();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threads; i++)
list.add(new Thread(() ->
for (int j = 0; j < times; j++)
longAdder.increment();
));
for (Thread thread : list)
thread.start();
for (Thread thread : list)
thread.join();
System.out.println("LongAdder value is : " + longAdder.longValue());
输出结果:
这里可以看到随着并发的增加,AtomicLong性能是急剧下降的,耗时是LongAdder的数倍。至于原因我们还是接着往后看。
LongAdder为什么这么快
先看下LongAdder的操作原理图
既然说到LongAdder可以显著提升高并发环境下的性能,那么它是如何做到的?
1、 设计思想上,LongAdder采用**"分段"的方式降低CAS失败的频次**
这里先简单的说下LongAdder的思路,后面还会详述LongAdder的原理。
我们知道,AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点数据,也就是N个线程竞争一个热点。
LongAdder的基本思路就是分散热点,将value值的新增操作分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个value值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。
LongAdder有一个全局变量volatile long base值,当并发不高的情况下都是通过CAS来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率。
例如当前类中base = 10,有三个线程进行CAS原子性的+1操作,线程一执行成功,此时base=11,线程二、线程三执行失败后开始针对于Cell[]数组中的Cell元素进行+1操作,同样也是CAS操作,此时数组index=1和index=2中Cell的value都被设置为了1.
执行完成后,统计累加数据:sum = 11 + 1 + 1 = 13,利用LongAdder进行累加的操作就执行完了,流程图如下:
如果要获取真正的long值,只要将各个槽中的变量值累加返回。这种分段的做法类似于JDK7中ConcurrentHashMap的分段锁。
2、使用Contended注解来消除伪共享
在 LongAdder 的父类 Striped64 中存在一个 volatile Cell[] cells; 数组,其长度是2 的幂次方,每个Cell都使用 @Contended 注解进行修饰,而**@Contended注解可以进行缓存行填充,从而解决伪共享问题**。伪共享会导致缓存行失效,缓存一致性开销变大。
@sun.misc.Contended static final class Cell
其中 Cell 即为累加单元
这里说的伪共享是指的缓存伪共享 ,这个是什么概念?
我们不得不从缓存说起: 大家都知道缓存的速度比内存的速度快很多
因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
个人简单总结下: @sun.misc.Contended 就是让缓存行只存储一个对象或者字段,避免一个失效后影响另外一个对象或者字段
3、惰性求值
LongAdder只有在使用longValue()获取当前累加值时才会真正的去结算计数的数据,longValue()方法底层就是调用sum()方法,对base和Cell数组的数据累加然后返回,做到数据写入和读取分离。
而AtomicLong使用incrementAndGet()每次都会返回long类型的计数值,每次递增后还会伴随着数据返回,增加了额外的开销。
LongAdder实现原理
之前说了,AtomicLong是多个线程针对单个热点值value进行原子操作。而LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。
比如有三个线程同时对value增加1,那么value = 1 + 1 + 1 = 3
但是对于LongAdder来说,内部有一个base变量,一个Cell[]数组。
base变量:非竞态条件下,直接累加到该变量上
Cell[]数组:竞态条件下,累加个各个线程自己的槽Cell[i]中
最终结果的计算是下面这个形式:
value = base +
LongAdder源码剖析
LongAdder 父类中有这么几个关键的属性
LongAdder源码
public void increment()
add(1L);
public void add(long x)
Cell[] as; long b, v; int m; Cell a;
//进入if的两个条件
//1.as有值,表示已经发生过竞争
//2.casBase() 失败,给base累加失败,表示存在竞争
if ((as = cells) != null || !casBase(b = base, b + x))
//uncontended 表示没有竞争
boolean uncontended = true;
if (
//cell[] 还没创建
as == null || (m = as.length - 1) < 0 ||
//当前线程的对应cell还没有
(a = as[getProbe() & m]) == null ||
//当前线程的cell累加失败 a为当前线程的cell
!(uncontended = a.cas(v = a.value, v + x)))
//进入cell数组创建,cell创建
longAccumulate(x, null, uncontended);
一般我们进行计数时都会使用increment()方法,每次进行+1操作,increment()会直接调用add()方法。
变量说明:
- as 表示cells引用
- b 表示获取的base值
- v 表示 期望值,
- m 表示 cells 数组的长度
- a 表示当前线程命中的cell单元格
条件分析:
条件一:as == null || (m = as.length - 1) < 0
此条件成立说明cells数组未初始化。如果不成立则说明cells数组已经完成初始化,对应的线程需要找到Cell数组中的元素去写值。
条件二:(a = as[getProbe() & m]) == null
getProbe()获取当前线程的hash值,m表示cells长度-1,cells长度是2的幂次方数,原因之前也讲到过,与数组长度取模可以转化为按位与运算,提升计算性能。
当条件成立时说明当前线程通过hash计算出来数组位置处的cell为空,进一步去执行longAccumulate()方法。如果不成立则说明对应的cell不为空,下一步将要将x值通过CAS操作添加到cell中。
条件三:!(uncontended = a.cas(v = a.value, v + x)
主要看a.cas(v = a.value, v + x),接着条件二,说明当前线程hash与数组长度取模计算出的位置的cell有值,此时直接尝试一次CAS操作,如果成功则退出if条件,失败则继续往下执行longAccumulate()方法。
接着往下看核心的longAccumulate()方法,代码很长,后面会一步步分析,先上代码:
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended)
int h;
//当前线程没有对应的cell,需要随机生成一个h值,绑定到当前线程的cell上
if ((h = getProbe()) == 0)
//初始化 probe
ThreadLocalRandom.current(); // force initialization
//h对象新的probe值,用来对应cell
h = getProbe();
wasUncontended = true;
//collide 为true 说明cells 需要扩容
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)
if ((a = as[(n - 1) & h]) == null)
if (cellsBusy == 0) // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy())
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;
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))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy())
try
if (cells == as) // Expand table unless stale
Cell[] rs = new Cell[n << 1];
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);
else if (cellsBusy == 0 && cells == as && casCellsBusy())
boolean init = false;
try // Initialize table
if (cells == as)
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
finally
cellsBusy = 0;
if (init)
break;
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
代码很长,if else分支很多,除此看肯定会很头疼。这里一点点分析,然后结合画图一步步了解其中实现原理。
我们首先要清楚执行这个方法的前置条件,它们是或的关系,如上面条件一、二、三
- cells数组已经初始化
- cells数组未初始化,尝试给CellsBusy加锁
- cells数组未经初始化,尝试给CellsBusy加锁失败,尝试给base加锁
longAccumulate()方法的入参:
long x 需要增加的值,一般默认都是1
LongBinaryOperator fn 默认传递的是null
wasUncontended竞争标识,如果是false则代表有竞争。只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false
然后再看下Striped64中一些变量或者方法的定义:
base: 类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
collide:表示扩容意向,false 一定不会扩容,true可能会扩容。
cellsBusy:初始化cells或者扩容cells需要获取锁, 0:表示无锁状态 1:表示其他线程已经持有了锁
casCellsBusy(): 通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
getProbe(): 获取当前线程的hash值
advanceProbe(): 重置当前线程的hash值
private static final long PROBE;
if ((h = getProbe()) == 0)
ThreadLocalRandom.current();
h = getProbe();
wasUncontended = true;
static final int getProbe()
return UNSAFE.getInt(Thread.currentThread(), PROBE);
我们上面说过getProbe()方法是为了获取当前线程的hash值,具体实现是通过UNSAFE.getInt()实现的,PROBE是在初始化时候获取当前线程threadLocalRandomProbe的值。
注:Unsafe.getInt()有三个重载方法getInt(Object o, long offset)、getInt(long address) 和getIntVolatile(long address),都是从指定的位置获取变量的值,只不过第一个的offset是相对于对象o的相对偏移量,第二个address是绝对地址偏移量。如果第一个方法中o为null是,offset也会被作为绝对偏移量。第三个则是带有volatile语义的load读操作。
如果当前线程的hash值h=getProbe()为0,0与任何数取模都是0,会固定到数组第一个位置,所以这里做了优化,使用ThreadLocalRandom为当前线程重新计算一个hash值。最后设置wasUncontended = true,这里含义是重新计算了当前线程的hash后认为此次不算是一次竞争。hash值被重置就好比一个全新的线程一样,所以设置了竞争状态为true。
可以画图理解为:
接着执行for循环,我们可以把for循环代码拆分一下,每个if条件算作一个CASE来分析:
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended)
for (;;)
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0)
else if (cellsBusy == 0 && cells == as && casCellsBusy())
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
如上所示,第一个if语句代表CASE1,里面再有if判断会以CASE1.1这种形式来讲解,下面接着的else if为CASE2, 最后一个为CASE3
CASE1执行条件:
if ((as = cells) != null && (n = as.length) > 0)
cells数组不为空,且数组长度大于0的情况会执行CASE1,CASE1的实现细节代码较多,放到最后面讲解。
我们这里先分析 else if (cellsBusy == 0 && cells == as && casCellsBusy())
else if (cellsBusy == 0 && cells == as && casCellsBusy())
boolean init = false;
try // Initialize table
if (cells == as)
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
finally
cellsBusy = 0;
if (init)
break;
CASE2 标识cells数组还未初始化,因为判断cells == as,这个代表当前线程到了这里获取的cells还是之前的一致。我们可以先看这个case,最后再回头看最为麻烦的CASE1实现逻辑。
cellsBusy上面说了是加锁的状态,初始化cells数组和扩容的时候都要获取加锁的状态,这个是通过CAS来实现的,为0代表无锁状态,为1代表其他线程已经持有锁了。cellsas代表当前线程持有的数组未进行修改过,casCellsBusy()通过CAS操作去获取锁。但是里面的if条件又再次判断了cellas,这一点是不是很奇怪?通过画图来说明下问题:
如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell[] rs = new Cell[2]表示数组的长度为2,rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1。h & 1类似于我们之前HashMap或者ThreadLocal里面经常用到的计算散列桶index的算法,通常都是hash & (table.len - 1),这里就不做过多解释了。 执行完成后直接退出for循环。
CASE3执行条件和实现原理:
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
进入到这里说明cells正在或者已经初始化过了,执行caseBase()方法,通过CAS操作来修改base的值,如果修改成功则跳出循环,这个CASE只有在初始化Cell数组的时候,多个线程尝试CAS修改cellsBusy加锁的时候,失败的线程会走到这个分支,然后直接CAS修改base数据。
CASE1 实现原理:
分析完了CASE2和CASE3,我们再折头回看一下CASE1,进入CASE1的前提是:cells数组不为空,已经完成了初始化赋值操作。
接着还是一点点往下拆分代码,首先看第一个判断分支CASE1.1:
if ((a = as[(n - 1) & h]) == null)
if (cellsBusy == 0)
Cell r = new Cell(x);
if (cellsBusy == 0 && casCellsBusy())
boolean created = false;
try
Cell[] rs; int m, j;
if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null)
rs[j] = r;
created = true;
finally
cellsBusy = 0;
if (created)
break;
continue;
collide = false;
这个if条件中(a = as[(n - 1) & h]) == null代表当前线程对应的数组下标位置的cell数据为null,代表没有线程在此处创建Cell对象。
接着判断cellsBusy==0,代表当前锁未被占用。然后新创建Cell对象,接着又判断了一遍cellsBusy == 0,然后执行casCellsBusy()尝试通过CAS操作修改cellsBusy=1,加锁成功后修改扩容意向collide = false;
for (;;)
if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null)
rs[j] = r;
created = true;
if (created)
break;
continue;
上面代码判断当前线程hash后指向的数据位置元素是否为空,如果为空则将cell数据放入数组中,跳出循环。如果不为空则继续循环。
继续往下看代码,CASE1.2:
else if (!wasUncontended)
wasUncontended = true;
h = advanceProbe(h);
wasUncontended表示cells初始化后,当前线程竞争修改失败wasUncontended =false,这里只是重新设置了这个值为true,紧接着执行advanceProbe(h)重置当前线程的hash,重新循环。
接着看CASE1.3:
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
进入CASE1.3说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环。
else if (n >= NCPU || cells != as)
collide = false;
如果cells数组的长度达到了CPU核心数,或者cells扩容了,设置扩容意向collide为false并通过下面的h = advanceProbe(h)方法修改线程的probe再重新尝试
至于这里为什么要提出和CPU数量做判断的问题:每个线程会通过线程对cells[threadHash%cells.length]位置的Cell对象中的value做累加,这样相当于将线程绑定到了cells中的某个cell对象上,如果超过CPU数量的时候就不再扩容是因为CPU的数量代表了机器处理能力,当超过CPU数量时,多出来的cells数组元素没有太大作用。
接着看CASE1.5:
else if (!collide)
collide = true;
如果扩容意向collide是false则修改它为true,然后重新计算当前线程的hash值继续循环,在CASE1.4中,如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意向collide=false,这里的意义是保证扩容意向为false后不再继续往后执行CASE1.6的扩容操作。
接着看CASE1.6分支:
else if (cellsBusy == 0 && casCellsBusy())
try
if (cells == as)
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
finally
cellsBusy = 0;
collide = false;
continue;
这里面执行的其实是扩容逻辑,首先是判断通过CAS改变cellsBusy来尝试加锁,如果CAS成功则代表获取锁成功,继续向下执行,判断当前的cells数组和最先赋值的as是同一个,代表没有被其他线程扩容过,然后进行扩容,扩容大小为之前的容量的两倍,这里用的按位左移1位来操作的。
Cell[] rs = new Cell[n << 1];
扩容后再将之前数组的元素拷贝到新数组中,释放锁设置cellsBusy = 0,设置扩容状态,然后继续循环执行。
到了这里,我们已经分析完了longAccumulate()所有的逻辑,逻辑分支挺多,仔细分析看看其实还是挺清晰的,流程图如下:
我们再举一些线程执行的例子里面场景覆盖不全,大家可以按照这种模式自己模拟场景分析代码流程:
如有问题也请及时指出,我会第一时间更正,不胜感激!
LongAdder的sum方法
当我们最终获取计数器值时,我们可以使用LongAdder.longValue()方法,其内部就是使用sum方法来汇总数据的。
java.util.concurrent.atomic.LongAdder.sum():
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;
实现很简单,base + ,遍历cells数组中的值,然后累加。
AtomicLong可以弃用了吗?
看上去LongAdder的性能全面超越了AtomicLong,而且阿里巴巴开发手册也提及到 推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观 锁的重试次数),但是我们真的就可以舍弃掉LongAdder了吗?
当然不是,我们需要看场景来使用,如果是并发不太高的系统,使用AtomicLong可能会更好一些,而且内存需求也会小一些。
我们看过sum()方法后可以知道LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。
而在高并发统计计数的场景下,才更适合使用LongAdder。
总结
LongAdder中最核心的思想就是利用空间来换时间,将热点value分散成一个Cell列表来承接并发的CAS,以此来提升性能。
LongAdder的原理及实现都很简单,但其设计的思想值得我们品味和学习。
以上是关于03_LongAdder 源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Juc16_LongAdder引入原理Striped64分散热点思想深度解析LongAdder源码和AtomicLong区别