文章目录
一、LongAdder原理
1. LongAdder架构
LongAdder 是 Striped64 的子类,Striped64 是 Number 子类
public class LongAdder extends Striped64 {}
abstract class Striped64 extends Number {}
2. 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值
==============================================================
abstract class Striped64 extends Number {
// CPU数量,即Cells数组的最大长度
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 存放Cell的hash表,大小为2的幂
// 这里的Cell是Striped64的静态内部类,懒惰初始化
transient volatile Cell[] cells;
/*
1.在开始没有竞争的情况下,将累加值累加到base;
2.在cells初始化的过程中,cells处于不可用的状态,这时候也会尝试将通过cas操作值累加到base
*/
transient volatile long base;
/*
cellsBusy,它有两个值0或1,它的作用是当要修改cells数组时加锁,
防止多线程同时修改cells数组(也称cells表),0为无锁,1位加锁,加锁的状况有三种:
(1)cells数组初始化的时候;
(2)cells数组扩容的时候;
(3)如果cells数组中某个元素为null,给这个位置创建新的Cell对象的时候;
*/
transient volatile int cellsBusy;
}
3. LongAdder为什么快?
- LongAdder基本思路就是分散热点,将value分散到一个Cell数组中,不同线程会命中数组的不同槽位中,各个线程只对自己槽位的value进行CAS操作,这样热点就被分散了,冲突概率小了
- sum()会将所有Cell数组中的value和base累加作为返回值,核心思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点
- 内部有一个base+一个Cell[ ]数组
- base变量:低并发,直接累加到该变量上
- Cell[ ]数组:高并发,累加到各个线程自己的槽Cell[i]中
- LongAdder在无竞争的情况下,跟AtomicLong一样, 对同一个base进行操作
- 当出现竞争时,则采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value拆分进这个数组cells
- 多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作
- 当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果
4. LongAdder之缓存行伪共享
(1)Striped64中静态内部类Cell源码
abstract class Striped64 extends Number {
// 防止缓存行伪共享 注解
@sun.misc.Contended
static final class Cell { // Cell 即为累加单元
// 保存累加结果
volatile long value;
// 构造方法为value赋初始值
Cell(long x) { value = x; }
// 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
final boolean cas(long prev, long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
// 省略其他代码
}
}
(2)CPU缓存结构
(3)缓存与内存的速度比较
从 cpu 到 | 大约需要的时钟周期 |
---|---|
寄存器 | 1 cycle (4GHz 的 CPU 约为0.25ns) |
L1 | 3~4 cycle |
L2 | 10~20 cycle |
L3 | 40~45 cycle |
内存 | 120~240 cycle |
- 由于CPU的速度远远大于内存速度,所以需要靠预读数据至缓存来提升效率
- CPU Cache分成了三个级别:L1,L2,L3。级别越小越接近CPU, 所以速度越快, 但是容量越小
- CPU获取数据会依次从L1,L2,L3中查找,如果都找不到则会直接从内存查找
- 而缓存是以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)
- 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
- CPU 需要保证数据的一致性,所以如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
例如:线程1将数据 x 从内存读到缓存行,线程2也将数据 x 从内存读到缓存行,当线程1将x修改为y,并设置到内存中,则线程2的整个缓存行数据需要失效,重新从内存中读取y
分析
static final class Cell { // 对象头16字节
volatile long value; // long 8字节
}
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 对象为 24 字节(16 字节的对象头和 8 字节的 long 类型的 value),缓存行一般存64字节,所以可以存下 2 个 Cell 对象(Cell[0]、Cell[1])。
这样问题来了,假设:
- Core-0 要修改 Cell[0]
- Core-1 要修改 Cell[1]
- 无论谁修改成功,都会导致对方 Core 的缓存行失效
- 比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 只累加Cell[0],累加后Core-0 中 Cell[0]=6001, Cell[1]=8000
- 虽然Core-0 中 Cell[1]=8000没累加,但是 Cell[0]、 Cell[1]是在同一个缓存行里,所以Core-1的缓存行需要整行失效,重新从头读取Cell[0]=6001,Cell[1]=8000,这就出现了伪共享问题,从而影响效率
- @sun.misc.Contended 就是用来解决伪共享,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding(空白,用来占位),从而让 CPU 将对象预读至缓存时,占用不同的缓存行(例如:让Cell[0]、Cell[1]占用不同的缓存行,占用Cell[0]变了不会影响Cell[1]),这样,不会造成对方缓存行的失效
伪共享(False Sharing)是指多个线程同时读写同一个缓存行的不同变量时,导致 CPU缓存失效。尽管这些变量之间没有任何关系,但由于在主内存中邻近,存在于同一个缓存行之中,它们的相互覆盖会导致频繁的缓存未命中,引发性能下降。
5. LongAdder 源码
(1)add方法
对外调用
LongAdder adder = new LongAdder();
adder.increment();
内部调用 add 方法源码
long x: 累加值
Cell[] as: 累加单元数组cells的引用
long b: 获取的base值
long v: 期望值(当前cell存储的值)
int m: cells数组的长度
Cell a: 当前线程命中的cell单元格
boolean uncontended: 表示cell是否没有竞争
========================================================================
public class LongAdder extends Striped64 implements Serializable {
public void increment() {
add(1L);
}
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
// 进入 if 的两个条件
// 条件1:as 有值, 表示已经发生过竞争, 进入 if
// 条件2:cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
// 由于 cells 是懒惰初始化,刚开始是null,会先进行casBase操作
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
/*
条件1: cells为空,说明正在出现竞争,上面是从条件2过来的,说明!casBase(b = base, b + x))=true
会通过调用longAccumulate(x, null, uncontended)新建一个数组,默认长度是2
条件2: 默认会新建一个数组长度为2的数组,m = as.length - 1 < 0 应该不会出现
条件3: 当前线程所在的cell为空,说明当前线程还没有更新过cell,uncontended为true,应初始化一个cell
条件4: 更新当前线程所在的cell失败,说明竞争很激烈,多个线程hash到同一个Cell,uncontended为false,应扩容
**/
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); // 创建cells数组的流程
}
}
}
add 各种场景:
- cells = null,casBase(b = base, b + x)累加失败,执行 longAccumulate(x, null, uncontended);
- cells != null,as[getProbe() & m] == null 表示当前线程还没创建cell,执行 longAccumulate(x, null, uncontended);
- cells != null,如果as[getProbe() & m] != null 表示当前线程创建了cell,执行累加单元cell的cas操作:a.cas(v = a.value, v + x),失败则执行 longAccumulate(x, null, uncontended);
小结:
- 最开始只更新base
- 更新失败,则新创建一个Cell[]数组
- 多个线程竞争到同一个Cell时,进行Cell[]数组扩容
(2)继承Striped64中的 longAccumulate方法
前置知识1:getProbe()方法获取线程hash值
// Striped64中的longAccumulate方法
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
// 存储线程的hash值,有了hash值就可以知道当前线程进入哪个槽位
int h;
// 如果getProbe()方法返回0,说明随机数未初始化,需要初始化后,线程才能进入对应槽位
if ((h = getProbe()) == 0) {
// 使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
ThreadLocalRandom.current();
// 重新获取hash值
h = getProbe();
// 重新获取hash值后,认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈,所以wasUncontended竞争状态为true
wasUncontended = true;
}
// Striped64中的getProbe方法,得到当前线程的hash值PROBE
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
前置知识2:longAccumulate方法总体逻辑
注意:执行顺序是从CASE2开始,CASE3是兜底方案,当大量线程同时进行CASE2时,只有一个线程能进入CASE2,失败的则进入CASE3
① 第一个线程创建cells 并初始化 一个累加单元cell
long x: 累加值
LongBinaryOperator fn: 默认null
boolean wasUncontended: 是否没竞争,false表示有竞争;只有cells初始化之后,并且CAS累加值失败,才为false
long base: 类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
boolean collide: 表示扩容意向,false一定不会扩容,true可能会扩容
cellsBusy: 初始化cells或者扩容cells需要获取锁,0表示无锁状态,1表示其他线程已经持有了锁
casCellsBusy: 通过CAS操作修改cellsBusy的值, CAS成功代表获取锁,返回true
NCPU: 当前计算机CPU数量,Cell数组扩容时会使用到
getProbe(): 获取当前线程的hash值
advanceProbe(): 重置当前线程的hash值
=================================================================================================
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
// 获取线程hash值,前面已经分析过,将代码隐藏
if ((h = getProbe()) == 0) {...}
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// 后面分析第一个if,先将代码隐藏
if ((as = cells) != null && (n = as.length) > 0) {...}
// 创建cells的场景
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) { // 再次检查cells引用是否改变,双重检测是为了避免并发场景下,重复创建cells
Cell[] rs = new Cell[2]; // 创建长度为2的cell数组
rs[h & 1] = new Cell(x); // 将累加值x随机存放到cell数组对应的索引下标位置
cells = rs; // 再将创建的cell数组引用赋值给cells
init = true;
}
} finally {
cellsBusy = 0; // 创建完cell数组后,解锁
}
if (init)
break; // 退出循环
}
// 尝试cas累加
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
- 进入for循环,先进入第一个else if,cellsBusy == 0 还未加锁,cells == as 表示cells是当前线程的引用,其他线程还未创建cells,casCellsBusy()方法通过cas尝试将cellsBusy改为1表示加锁,加锁成功,其他线程就不会来干扰cells的创建
- casCellsBusy()方法通过cas尝试加锁失败后,进入第二个else if,casBase()方法尝试cas累加,成功则返回,失败则重新进入for循环
② 在第一个线程基础上,第二个线程赋值给cell中的空槽位
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {...}
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
// 分析第一个if
if ((as = cells) != null && (n = as.length) > 0) { // cells已经被创建
if ((a = as[(n - 1) & h]) == null) {// cells虽然创建了,但是其中累加单元cell[0]或cell[1]还没被其他线程使用,则进入该if
if (cellsBusy == 0) { // 其他线程没使用,自然也就没加锁
Cell r = new Cell(x); // 创建累加单元,还没赋值到cells数组中
// 该if是将创建的累加单元,设置到cells数组的空位置(cell[0]或cell[1]位置)
// 双重检测cellsBusy == 0,避免并发场景时重复赋值
if (cellsBusy == 0 && casCellsBusy()) {// 进入该if之前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 (cellsBusy == 0 && cells == as && casCellsBusy()) {...}
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
③ 线程获取到了已经有值的累加单元cell,并在该cell上尝试进行累加
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {...}
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
// 已经分析过
if ((a = as[(n - 1) & h]) == null) {...}
else if (!wasUncontended) // wasUncontended为false说明在同一个槽位竞争失败
wasUncontended = true; // 跳到下面h = advanceProbe(h)位置,重新hash换一个槽位累加
// 尝试对已经有值的累加单元cell进行累加,成功则退出
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
// 累加失败,判断是否超过CPU上限
else if (n >= NCPU || cells != as)
// 这个设计很巧妙
// 超过CPU上限后,设置collide = false,为了让下次循环进入 else if (!collide)而不是进入下面的else if (cellsBusy == 0 && casCellsBusy()),防止进行扩容
// 跳到下面h = advanceProbe(h)位置,重新hash换一个槽位累加
collide = false;
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {// 其他线程没加锁,当前线程进入时再自己加锁
try {
// 对cells进行扩容
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);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {...}
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
小结:longAccumulate方法流程图
(3)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;
}
高并发下sum的值不精确的原因
sum执行时,并没有限制对base和cells的更新。所以LongAdder不是强一致性,而是最终一致性
- 首先,最终返回的sum局部变量,初始被赋值为base,在最终返回时,可能有其他线程再次更新base,而此时局部变量sum不会更新,造成不一致
- 其次,这里对cell的读取也无法保证是最后一次写入的值
(4)LongAdder 总结
- 采用分段CAS降低重试频率(这种分段的做法类似于JDK7中ConcurrentHashMap的分段锁)
- 高并发环境下,value变量其实是一个热点数据,也就是N个线程竞争一个热点。
- LongAdder的基本思路就是分散热点,将value值的新增操作分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个value值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。
- LongAdder有一个全局变量volatile long base值,当并发不高的情况下都是通过CAS来直接操作base值,如果CAS失败,则针对LongAdder中的Cell[]数组中的Cell进行CAS操作,减少失败的概率。
- 统计累加数据:sum() = base + Cell[]中的各Cell对象的value值之和。
- 而AtomicLong是多个线程针对单个热点value值进行原子操作。
- 惰性求值
- LongAdder只有在使用longValue()获取当前累加值时才会真正的去结算计数的数据,longValue()方法底层就是调用sum()方法,对base和Cell数组的数据累加然后返回,做到数据写入和读取分离。
- 而AtomicLong使用incrementAndGet()每次都会返回long类型的计数值,每次递增后还会伴随着数据返回,增加了额外的开销。
- AtomicLong VS LongAdder
AtomicLong:
- AtomicLong实现原理是基于CAS+自旋操作,CAS是基于硬件来实现原子性,保障线程安全。
- AtomicLong使用场景:低并发下的全局计数器、序列号生成器。
- AtomicLong优势:占用空间小;缺点:高并发下性能急剧下降(N个线程同时进行自旋,N-1个线程会自旋失败、不断重试)。
LongAdder:
- LongAdder设计思想:空间换时间,分散value值的热点数据;实现原理:高并发时采用Cell数组进行分段CAS。
- LongAdder使用场景:高并发下的全局计数器。
- LongAdder优势:能减少CAS重试次数、能防止伪共享、惰性求值;缺点:使用sum统计时如果有并发更新,可能导致统计的数据有误差。