jdk8 以后,新增了几个专门用来做累加的类,它们累加的性能要比 Atomic 类高很多。
/**
* Atomic和LongAdder耗时测试
*/
public class Main {
public static void main(String[] args) throws Exception{
testAtomicLongAdder(1, 10000000); // 1个线程 共累加1千万次
testAtomicLongAdder(10, 10000000); // 10个线程 共累加1千万次
testAtomicLongAdder(100, 10000000); // 100个线程 共累加1千万次
}
static void testAtomicLongAdder(int threadCount, int times) throws Exception{
System.out.println("threadCount: " + threadCount + ", times: " + times);
long start = System.currentTimeMillis();
testLongAdder(threadCount, times);
System.out.println("LongAdder 耗时:" + (System.currentTimeMillis() - start) + "ms");
System.out.println("threadCount: " + threadCount + ", times: " + times);
long atomicStart = System.currentTimeMillis();
testAtomicLong(threadCount, times);
System.out.println("AtomicLong 耗时:" + (System.currentTimeMillis() - atomicStart) + "ms");
System.out.println("----------------------------------------");
}
static void testAtomicLong(int threadCount, int times) throws Exception{
AtomicLong atomicLong = new AtomicLong();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadCount; 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());
}
static void testLongAdder(int threadCount, int times) throws Exception{
LongAdder longAdder = new LongAdder();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < threadCount; 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());
}
}
输出
threadCount: 1, times: 10000000
LongAdder value is : 10000000
LongAdder 耗时:181ms
threadCount: 1, times: 10000000
AtomicLong value is : 10000000
AtomicLong 耗时:106ms
----------------------------------------
threadCount: 10, times: 10000000
LongAdder value is : 100000000
LongAdder 耗时:364ms
threadCount: 10, times: 10000000
AtomicLong value is : 100000000
AtomicLong 耗时:1769ms
----------------------------------------
threadCount: 100, times: 10000000
LongAdder value is : 1000000000
LongAdder 耗时:1500ms
threadCount: 100, times: 10000000
AtomicLong value is : 1000000000
AtomicLong 耗时:17597ms
----------------------------------------
在线程数只有 1 个的时候,LongAdder 并未体现出优势;
但随着线程数的增加,LongAdder 和 AtomicLong 的差距就相当明显了,AtomicLong 的性能急剧下降,耗时是 LongAdder 的数倍。
由此可看出 LongAdder 在多线程高并发下的数据统计性能非常优秀。
性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。
LongAdder 和 LongAccumulator 区别:
相同点:
不同点:
调用 casBase 时
function.applyAsLong(b = base, x)
来计算casBase(b = base, b + x)
来计算LongAccumulator 类功能更加强大,构造方法参数中
LongAccumulator 和LongAdder 的实现方式是完全一样的,只是做了一些定制。通过 LongBinaryOperator
函数,后面我们在看源码的时候会看到,二者唯一的不同就是计算是用的值还是用的 LongBinaryOperator
来计算。LongAccumulator 性能慢一些,是因为 Lambda 表达式(方法引用)会额外的创建一些对象。
LongAdder 是 Java8 提供的类,跟 AtomicLong 有相同的效果,但对 CAS 机制进行了优化,尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能。
CAS 底层实现是在一个循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈修改成功率很高,否则失败率很高,失败后这些重复的原子性操作会耗费性能(导致大量线程空循环,自旋转)。
优化核心思想:数据分离,将 AtomicLong 的单点的更新压力分担到各个节点,空间换时间,在低并发的时候直接更新,可以保障和 AtomicLong 的性能基本一致,而在高并发的时候通过分散减少竞争,提高了性能。
分段 CAS 机制:
自动分段迁移机制:某个 Cell 的 value 执行 CAS 失败,就会自动寻找另一个 Cell 分段内的 value 值进行 CAS 操作。
一个缓存行加入了多个 Cell 对象叫做伪共享。
Cell 为累加单元:数组访问索引是通过 Thread 里的 threadLocalRandomProbe 域取模实现的,这个域是 ThreadLocalRandom 更新的
// Striped64.Cell
// @sun.misc.Contended:防止缓存行伪共享
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
// 最重要的方法 用 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略不重要代码
}
得从缓存说起
缓存与内存的速度比较
从 cpu 到 | 大约需要的时钟周期 |
---|---|
寄存器 | 1 cycle (4GHz 的 CPU 约为0.25ns) |
L1 | 3~4 cycle |
L2 | 10~20 cycle |
L3 | 40~45 cycle |
内存 | 120~240 cycle |
因为 Cell 是数组形式,在内存中是连续存储的,64 位系统中,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),每一个 cache line 为 64 字节,因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效。
@sun.misc.Contended
用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。
LongAdder 的结构比较简单,如下图
其由 base 值 (long类型) 和 Cell 数组构成,如上图
当没有 base 的更新没有线程竞争的时候,会直接写到 base 里面去,而不会操作 Cell 数组,当 base 的写出现了竞争的时候,就会创建 Cell 数组,由不同的线程写不同的下标。当最后求和的时候,通过上述的公式,sum = base + 所有槽的值
。
s u m = b a s e + ∑ i = 0 n C e l l [ i ] sum=base+\sum_{i=0}^{n} Cell[i] sum=base+i=0∑nCell[i]
执行流程图
刚开始第一次去理解上述的内容比较抽象,现在从源码开始讲起。
LongAdder 的属性,在其父类 Striped64 中
abstract class Striped64 extends Number {
/*
* @sun.misc.Contended 添加缓存填充,用来消除伪共享
* 伪共享会导致缓存行失效,缓存一致性开销变大。
* 伪共享指的是多个线程同时读写同一个缓存行的不同变量时导致的 CPU缓存失效。
* 尽管这些变量之间没有任何关系,但由于在主内存中邻近,存在于同一个缓存行之中,
* 它们的相互覆盖会导致频繁的缓存未命中,引发性能下降。这里对于伪共享我只是提一下概念,并不会深入去讲解,大家可以自行查阅一些资料。
* 解决伪共享的方法一般都是使用直接填充,我们只需要保证不同线程的变量存在于不同的 CacheLine 即可,
* 使用多余的字节来填充可以做点这一点,这样就不会出现伪共享问题。在Disruptor队列的设计中就有类似设计。
*/
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset; // 偏移量
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class; // 拿到Cell对象
valueOffset = UNSAFE.objectFieldOffset // 获取偏移地址
(ak.getDeclaredField("value")); // 获取value字段
} catch (Exception e) {
throw new Error(e);
}
}
}
/** Number of CPUS, to place bound on table size */
// 表示当前计算机CPU核心数,作用 => 控制cells数组长度的一个关键条件
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* Table of cells. When non-null, size is a power of 2.
* Cells 数组 大小是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.
* base 基础 value 值,当并发较低的时候,只累加该值,主要用于没有竞争的情况,通过CAS更新 | 当cells扩容时,需要将数据写入base中
*/
transient volatile long base;
/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
* 创建或者落入Cells数组的时候使用的自旋锁变量调整单元格大小,创建单元格时候使用的锁
* 初始化cells或者扩容cells需要获取锁 0表示无锁状态 1表示其他线程已经持有了锁
*/
transient volatile int cellsBusy;
}
工作流程:
cells 占用内存是相对比较大的,是惰性加载的,在无竞争或者其他线程正在初始化 cells 数组的情况下,直接更新 base 域
在第一次发生竞争时(casBase 失败)会创建一个大小为 2 的 cells 数组,将当前累加的值包装为 Cell 对象,放入映射的槽位上
分段累加的过程中,如果当前线程对应的 cells 槽位为空,就会新建 Cell 填充,如果出现竞争,就会重新计算线程对应的槽位,继续自旋尝试修改
分段迁移后还出现竞争就会扩容 cells 数组长度为原来的两倍,然后 rehash,数组长度总是 2 的 n 次幂,默认最大为 CPU 核数,但是可以超过,如果核数是 6 核,数组最长是 8
LongAdder#add()方法
我们通常使用的是 LongAdder 的 increment()
方法,也就是自增方法,底层 LongAdder 其实调用的是 add()
方法
public class LongAdder extends Striped64 implements Serializable {
/**
* Equivalent to {@code add(1)}.
* 自增 实际上调用的是 add(1L);
*/
public void increment() {
add(1L);
}
/**
* Equivalent to {@code add(-1)}.
* 自减 实际上调用的是 add(1L);
*/
public void decrement() {
add(-1L);
}
}
LongAdder继承Striped64,并且使用到了Striped64中的方法,这是一个公共方法,我们稍后再说。
现在来分析 LongAdder 的 add()
方法
public class LongAdder extends Striped64 implements Serializable {
/**
* Adds the given value.
* 1.casBase
* 2.casBase失败,创建cells
* 3.cells失败,扩容cells,扩容到CPU核心数大小
* @param x the value to add
*/
public void add(long x) {
// as 表示当前cells引用
// b 表示获取的base值
// v 表示期望值
// m 表示cells数组的长度
// a 表示当前线程命中的cell单元格
Cell[] as; long b, v; int m; Cell a;
// 条件1:true->表示cell已经初始化过了 当前线程应该将数据写入到对应的cell中
// false->表示第一次加载,cells未初始化,所有的数据应该尝试写到base中
// 条件2:true->表示当前线程CAS数据替换失败,发生竞争了,可能需要重试 或者 扩容,这里指的是 !casBase(x,y) 整体是否为true
// false->表示当前线程CAS成功
if ((as = cells) != null || !casBase(b = base, b + x)) {
// 什么时候会进来?
// 1.true->cells已经初始化,需要尝试将数据写入到对应的cell,注意,这里是尝试写入
// 2.true->发生竞争了,可能需要重试 或 扩容
// ture->未竞争 false->发生竞争
boolean uncontended = true;
// 第一次 as 为null
// 已经新建了数组了
// 如果某个calls槽cas失败,就可能进行扩容操作
// 如果第一次写发现值为null,初始化了cell,但是没有设置值。需要设置值
// 如果不为null,就进行cas操作
// 如果不为null,就进行cas操作 且 cas操作失败,然后扩容
// 条件1:as == null true->说明当前cells未初始化,也就是多线程写base发生竞争了
// false->说明当前cells已经始化了,当前线程应该是 找自己的cell 写值
// 条件2:(m = as.length - 1) < 0
// true->当前cells数组长度为0
// false->当前cells数组长度大于0
// m = cells数组长度,且长度一定为2的幂,后续会看到或参考HashMap的扩容
// 条件3:(a = as[getProbe() & m]) == null
// true->当前线程的cells[index]对应的数据为null,说明是第一次写值,需要创建 longAccumulate 支持
// false->说明当前线程对应的cell不为空,说明 下一步想要将x值 添加到cell中
// 捞一下as[getProbe() & m]:获取当前线程的hash值 与上 cells长度-1(m,上一个条件计算的),计算cells中的下标
// 条件4:!(uncontended = a.cas(v = a.value, v + x))
// true->当前 a = cells[index] cas写失败 此时 uncontended = false 发生竞争了
// false->表示cas成功
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
// 都有哪些情况会调用?
// 1.true->说明当前cells未初始化,也就是多线程写base发生竞争了[重试|初始化cells]
// 2.true->当前cells数组长度为0
// 3.true->当前线程的cells[index]对应的数据为null,说明是第一次写值,需要创建 longAccumulate 支持
// 4.true->当前 a = cells[index] cas写失败 此时 uncontended = false 发生竞争了[重试|扩容]
// 执行累加 调用父类Striped64的longAccumulate方法
longAccumulate(x, null, uncontended);
}
}
}
流程图
Striped64 提供了数据的 CAS 操作,和线程的 hash 处理,如下
abstract class Striped64 extends Number {
/**
* CASes the base field.
*/
final boolean casBase(long cmp, long val) {
// cas 进行更新 base值
// 返回是否更新成功,CAS只写一次
// 可能写失败,写失败就开始建立Cells
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
/**
* CASes the cellsBusy field from 0 to 1 to acquire lock.
* 通过 CAS 方式获取锁,操作 cellsBusy 的值 0 => 1
*/
final boolean casCellsBusy() {
return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}
/**
* Returns the probe value for the current thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
* 获取当前线程的hash值
*/
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
/**
* Pseudo-randomly advances and records the given probe value for the
* given thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
* 重置当前线程的hash值
*/
static final int advanceProbe(int probe) {
probe ^= probe << 13; // xorshift
probe ^= probe >>> 17;
probe ^= probe << 5;
UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
return probe;
}
}
在这个方法中,我们就提到了之前的
LongBinaryOperator
函数,实际在进行计算的时候,是判断是否有这个值来进行处理的。但我们通过 LongAdder 传过来的 LongBinaryOperator 这个值为 null,所以这里就不用考虑了。
Striped64#longAccumulate:cell 数组创建
abstract class Striped64 extends Number {
/**
* Handles cases of updates involving initialization, resizing,
* creating new Cells, and/or contention. See above for
* explanation. This method suffers the usual non-modularity
* problems of optimistic retry code, relying on rechecked sets of
* reads.
*
* @param x the value 期望的值
* @param fn the update function, or null for add (this convention
* avoids the need for an extra field or function in LongAdder).
* 执行更新的方法
* @param wasUncontended false if CAS failed before call // Cells 初始化之后,并当前线程CAS修改失败 为false
*
* 如何进来这个方法
* 1.cells未初始化并且casBase失败(多线程写base发生竞争了)[初始化cells] -> 进入CASE2 拿到锁了 则创建一个size为2的cells 没拿到锁 数据累加到base中
* 2.cells的对应的值为null,第一次写值,需要 longAccumulate 支持 -> 进入CASE1.1 创建一个Cell 赋值到对应的下标中
* 3.cells初始化并且当前线程cas失败,当前线程对应的cell有竞争[重试|扩容] -> 进入CASE1.2 因为之前发生过竞争了 所以需要将wasUncontended重新设置为true 重置当前hash值 再进入CASE1.1 判断依旧不为空 则进入CASE1.3 重试CAS 成功则退出 失败则进入CASE1.5把扩容意向改为true 再rehash 进行自旋 如果CASE1.3依旧CAS失败 则进行CASE1.6扩容逻辑 所以如果执行到扩容 则至少需要3次CAS失败
*/
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// h: 线程hash值
int h;
// 还没有给线程分配hash值,这个时候先分配一手hash值
if ((h = getProbe()) == 0) {
// 等于0 强制设置为当前线程,给当前线程分配 hash 值
ThreadLocalRandom.current(); // force initialization 强制
// 重新获取 probe 值,hash值被设置为一个全新的线程,所以设置了 wasUncontended 为true
h = getProbe();
// 重新计算了,认为不是热点 也即是说,线程第一次进来,计算的hash值为0,当前线程的hash值0与任何数据位运算都是0,
// 那么就会落到第一个cell,这是不合理的,所以需要将wasUncontended设置为true
// 也就是说,首次线程访问,先会写cell[0]号位置
// 写成功了,就没有下文了,写失败了,说明发生了竞争,那么就不适合再给线程设置为0了,因为已经发生了抢夺
// 不把它当做一次真正的竞争
wasUncontended = true;
}
// collide 表示扩容意向,false 一定不会扩容,true可能扩容
boolean collide = false; // True if last slot nonempty
// 自旋
for (;;) {
// as cells数组引用
// a 当前线程的命中的cell
// n cells数组长度
// v 表示期望值
Cell[] as; Cell a; int n; long v;
// CASE1:cells 已经初始化了,当前线程应该将数据写入到对应的cell中
if ((as = cells) != null && (n = as.length) > 0) {
// CASE1.1: 如果计算完成之后,计算当前的cell单元为null,说明这个cell没有被使用,则需要新建 new cell
if ((a = as[(n - 1) & h]) == null) {
// cellsBusy == 0 锁未被占用
// 如果cells数组没有再扩容
if (cellsBusy == 0) { // Try to attach new Cell
// 拿当前的x创建一个cell单元
Cell r = new Cell(x); // Optimistically create
// 尝试加锁
if (cellsBusy == 0 && casCellsBusy()) {
// 是否创建成功的标记
boolean created = false;
// 在有锁的情况下在检查一下之前的判断
try { // Recheck under lock
// rs 表示当前cells的引用
// m 表示cells长度
// j 表示当前线程命中的下标
Cell[] rs; int m, j;
// 条件1和条件2恒成立,因为进来的时候就已经判断了
// 这里大家可能会有疑惑,为毛没有再判断一次 cells == as
// 因为这个时候,两个线程A、B进来之后,A停在加锁之前,B进来了,那么A、B必然插入的是同一个槽
// 这时会判断 rs[j = (m - 1) & h] == null) 为了防止其它线程初始化过该位置 如果为null才赋值 否则就不管了
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;
// 这个时候目标槽已经不是空了 继续自旋
// LongAdder 必须保证每个值都写进去
continue; // Slot is now non-empty
}
}
// 扩容意向修改为false
// 因为当前线程还没写值,不一定写失败,所以扩容意向为false
collide = false;
}
// CASE1.2:只有一种情况会进来,cells初始化之后,当前线程竞争修改失败,并且已经初始化线程hash值
// wasUncontended目前false,这里是重新设置这个值为 true
// 紧接着执行 advanceProbe(h) 重置当前线程的hash,继续循环
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 如果为true。就继续执行当前cell位的cas操作
// CASE1.3:当前线程rehash过hash值,新命中的cell不为空,就来到这里,然后进行cas写
// 如果写成功了,跳出循环
// 如果写失败了,表示rehash之后,又写失败了,进入下一个条件CASE1.4
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// CASE1.4:
// 条件1:n 是数组长度 大于CPU核心数,扩容意向collide设置为false,表示不再扩容了
// 条件2:cells != as 其他线程已经扩容过了,当前线程rehash之后重试即可
else if (n >= NCPU || cells != as)
// 如果n大于等于 NCPU 不可扩容
collide = false; // At max size or stale
// CASE1.5:
// 设置扩容意向为true,但是不一定扩容成功
// 如果扩容意向为false,重新设置为true 然后继续执行循环
// 设置为true之后,再次rehash然后再次循环,如果又失败了,这里且不满足 (n >= NCPU || cells != as)
// 此时collide为true 执行 CASE1.6
else if (!collide)
collide = true;
// CASE1.6:扩容操作并加锁 注意:只是扩容,并没有进行赋值
// 条件1:cellsBusy == 0 表示当前没有占用锁,当前线程可以抢锁
// 条件2:casCellsBusy() 抢锁
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 和上面一样,需要判断,防止重复扩容
if (cells == as) { // Expand table unless stale
// 扩容2倍
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
// 扩容赋值
cells = rs;
}
} finally {
// 释放锁
cellsBusy = 0;
}
// 扩容意向设置为false
collide = false;
continue; // Retry with expanded table
}
// 重新设置线程hash值 h
h = advanceProbe(h);
}
// CASE2: cells 没加锁并且没有初始化(cells为null),则尝试进行加锁并且开始初始化操作
// 条件1:cellsBusy == 0 没有加锁
// 条件2:cells == as 为什么又对比一次?因为多线程场景下,A线程过来,发现是null,然后进入了,然后B线程过来,不一定是null
// 可能其他线程会修改了cells
// 条件3:true->表示cas获取锁成功,将cellsBusy设置为1,false->表示其他线程正在持有锁
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
// 初始化操作
boolean init = false;
try { // Initialize table
// 初始化的时候,进行创建cells数组
// casBase 失败,新建大小为2的cells数组
// 为毛又对比一下?因为是这样的,有可能在执行 cellsBusy == 0 && cells == as && casCellsBusy() 的时候
// 执行到 cellsBusy == 0 && cells == as 线程让出CPU 然后另外一个线程执行为true,那么其可以继续操作cells
// 此时在切换过来,这个cells就不是null了
// 防止其它线程已经初始化了,当前线程再次初始化 导致丢失数据
// 类似双重检查锁 - 这里是安全的,因为多线程场景下只有一个线程能够拿到锁,使用casCellsBusy()保证
if (cells == as) {
// 新建大小为2的cells数组
Cell[] rs = new Cell[2];
// 设置值
rs[h & 1] = new Cell(x);
// 赋值cells
cells = rs;
// 初始化成功
init = true;
}
} finally {
cellsBusy = 0; // 最后要释放锁
}
if (init)
break; // 初始化成功则退出自旋
}
// CASE3:cells 正在初始化 则尝试在base上进行累加
// 1.当前cellsBusy被别的线程持有(上锁),表示其它线程正在初始化cells,这个时候需要进行兜底操作,将当前线程值累加到base
// 2.cells被其他线程初始化后,需当前线程需要将数据要累加到base
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
}
流程图
每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)
当没有 base 的更新没有线程竞争的时候,会直接写到 base 里面去,而不会操作 Cell 数组,当 base 的写出现了竞争的时候,就会创建 Cell 数组,由不同的线程写不同的下标。当最后求和的时候,通过上述的公式,sum = base + 所有槽的值
。获取最终结果通过 sum 整合。
s u m = b a s e + ∑ i = 0 n C e l l [ i ] sum=base+\sum_{i=0}^{n} Cell[i] sum=base+i=0∑nCell[i]
保证最终一致性,不保证强一致性。
public class LongAdder extends Striped64 implements Serializable {
// 求和方法,只保证最终一致性
// 计算方式如上
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;
}
// 数值归零方法
public void reset() {
Cell[] as = cells; Cell a;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
a.value = 0L;
}
}
}
// 数值归零并返回sum值
public long sumThenReset() {
Cell[] as = cells; Cell a;
long sum = base;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null) {
sum += a.value;
a.value = 0L;
}
}
}
return sum;
}
}
参考
- 视频参考
- b站_小刘讲源码公开课 JDK8 新特性LongAdder源码深度讲解,保证让你学到很多硬核知识!
- b站_黑马程序员深入学习Java并发编程,JUC并发编程全套教程
- 文章参考
- 肆华_LongAdder阅读理解
- shstart _高并发计数器之LongAdder源码解析