自己在复习CAS相关时,突然想到了一个面试题:
项目乐观锁(超高并发下,怎么解决?)
比如:AtomicLong 使用内部变量 value 保存着实际的 long 值,所有的操作都是针对该 value 变量进
行。也就是说,高并发环境下,value 变量其实是一个热点,也就是 N 个线程竞争一个热点。重试
线程越多,意味着 CAS 的失败几率更高,CAS 失败几率就越高,从而进入恶性 CAS 空自旋状态。
LongAdder 的基本思路就是分散热点,将 value 值分散到一个数组中,不同线程会命中到数
组的不同槽(元素)中,各个线程只对自己槽中的那个值进行 CAS 操作。这样热点就被分散了,
冲突的概率就小很多。那么此时就想到了LongAdder,关于LongAdder的其他概念这里不再解释,直接上源码角度(Jdk_1.8.0_191),看看这个是什么套路?
先来看看LongAdder的add()方法:
public void add(long x) {
//cells 的引用
Cell[] as;
//b base v 期望值
long b, v;
//表示 cells 的长度
int m;
//a 表示当前线程命中的单元格
Cell a;
//条件一: true 表示cells已经初始化好了 当前线程应该将数据写入到对应的 cells 中;直接进入if代码块
// false 表示 cells 未被初始化 ,当前所有线程应该将数据写到 base 中;进入下一个判断条件
//条件二:false 表示当前线程替换数据成功;好的达到目的 直接退出代码块
// true 竞争发生 casBase 失败 需要扩容 或者 重试;进入if代码块
if ((as = cells) != null || !casBase(b = base, b + x)) {
//什么时候会进来?
//1. 条件一 成立 cells 已经初始化好了 当前线程应该将数据写入到对应的 cells 中
//2. 条件二 成立 cells 未被初始化 竞争发生失败 需要扩容 或者 重试
//true 未发生竞争 false 发生了竞争
boolean uncontended = true;
//条件一:(as == null || (m = as.length - 1) < 0
//成立:说明cell未被初始化 也就是多线程写base出现竞争了;直接接入if代码块
//不成立:说明cell已经被初始化了,当前线程应该是 找自己的cell的值;进入下一个判断条件
//条件二:getProbe() 获取当前线程的hash值
//成立:(cells已经开好了) 当前线程对应的下标元素为 null,需要创建;直接接入if代码块
//不成立:当前线程对应的下标元素不为null,说明下一步需要将 x 追加到 此cell 中; 进入下一个判断条件
//条件三:a.cas(v = a.value, v + x) :
//成立:说明cas赋值完成;取反 false 直接 退出代码块,完事儿了;
//不成立: 说明当前线程对应得 cell 存在竞争,一般uncontended都是false;进入if代码块
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x))) {
//进来这里的情况?
//1. 条件一 成立 说明 cell 未被初始化 也就是多线程写 base 出现竞争了 需要重试初始化 cells
//2. 条件二 成立 说明当前线程对应的下标元素为 null,需要创建;
//3. 条件三 成立 说明当前线程对应的 cell 存在竞争,需要重试;
longAccumulate(x, null, uncontended);
}
}
}
主要是 Striped64.longAccumulate() 在使劲:
//进来这里的情况?
//1.条件一 成立 说明cell未被初始化 也就是多线程写 base 出现竞争了需要重试初始化cells
//2.条件二 成立 说明cell已被初始化 当前线程对应的 cell 为 null,需要创建
//3.条件三 成立 说明cell已被初始化 当前线程对应的 cell 存在竞争,需要扩容
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
//表示线程hash值
int h;
//条件成立:说明还未分配 hash 值 需要重新分配hash值
// 这个if相当于给线程生成一个非0的hash值
if ((h = getProbe()) == 0) {
//给当前线程分配 hash 值
ThreadLocalRandom.current(); // force initialization
h = getProbe();
//默认 hash为 0 的线程肯定是写入到了 0 这个 槽位,不把它当作是真正的竞争
//true 未发生竞争 false 发生了竞争
wasUncontended = true;
}
//表示扩容 意向:false 一定不扩容 true 可能会扩容
boolean collide = false;
// 自旋
for (; ; ) {
//表示 cells 引用
Cell[] as;
//当前线程命中的cell
Cell a;
//表示cells 数组长度
int n;
//期望值
long v;
//CASE1 除非是 还未被初始化 转 CASE2
//条件一:成立:Cells 已经被初始化了,当前线程需要把数据写入到对应的 cell 中;进入当前if分支
// 不成立: 转363行代码 进行第二个判断 目的是为了初始化 cells
if ((as = cells) != null && (n = as.length) > 0) {
//进来的前提条件:
//2.条件二 成立 说明 cell 已被初始化 当前线程对应的 cell 为 null,需要创建
//3.条件三 成立 说明 cell 已被初始化 当前线程对应的 cell 存在竞争,需要重试|扩容
// 成立的话:表示当前线程对应的 cells 下标 cells 为空 需要创建 new Cell 实例
if ((a = as[(n - 1) & h]) == null) {
// 成立的话 表示当前是无锁状态 锁未占用
if (cellsBusy == 0) {
//新的cell 元素
Cell r = new Cell(x);
//是无锁状态 且 能获取到锁 且 尝试加锁(防止线程并发操作)
if (cellsBusy == 0 && casCellsBusy()) {
//是否创建 成功 标记
boolean created = false;
// Recheck under lock 在有锁的情况下再检测一遍之前的判断
try {
//表示当前 cells 引用
Cell[] rs;
//表示长度
int m, j;
//考虑别的线程可能执行了扩容,这里重新赋值重新判断
if ((rs = cells) != null && (m = rs.length) > 0 &&
//当 当前线程发生了退出cpu 又回到cpu 时
//为了防止其他线程在此期间初始化过该位置 然后当前线程再次初始化此位置
//导致 数据覆盖(如果被覆盖了就不需要覆盖了 需要再次寻址)
rs[j = (m - 1) & h] == null) {
//对没有使用的Cell单元进行累积操作(第一次赋值相当于是累积上一个操作数,
//求和时再和base执行一次运算就得到实际的结果)
rs[j] = r;
//创建完毕
created = true;
}
} finally {
//释放锁
cellsBusy = 0;
}
//如果原本为null的Cell单元是由自己进行第一次累积操作,
//那么任务已经完成了,所以可以退出循环
if (created) {
break;
}
//不是自己进行第一次累积操作,重头再来
continue; // Slot is now non-empty
}
}
//强制设置为不扩容
//执行这一句是因为cells被加锁了,不能往下继续执行第一次的赋值操作(第一次累积),
//所以还不能考虑扩容
collide = false;
}
// 此时 线程对应的 cell 元素不为空 只是上一个add()函数调用cas 赋值时 失败 就进来了这个分支
// wasUncontended :只有cells 初始化后 并且 当前线程 是 竞争修改失败 才会 false,
//也就是只会执行一次
//CAS already known to fail add()函数执行CAS更新a.value(进行一次累积)
//的尝试已经失败了,说明已经发生了线程竞争
else if (!wasUncontended) {
//情况失败标识,然后跳出此 if 去执行最后代码块的 重新算一遍线程的hash值 然后重新 循环
wasUncontended = true;
} // Continue after rehash
//线程对应位置不为空 && 上一个if分支也执行过了 线程也重新执行rehash了
//就来到了这里 再次尝试执行cas追加操作
//一般都是 fn == null 直接更新 v+x
//当前线程 rehash 过 新命中的 cell 不为空
//如果写入成功 就退出自旋 ;
// 否则 表示 rehash 之后 命中的新cell 也存在竞争 重试1次
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) {
//执行成功 完事了 要是没有成功就再继续rehash线程 继续循环
break;
}
//线程对应位置不为空 && 上一个if分支也执行过了 线程也重新执行rehash了 &&
//相应的新位置 cell 执行cas追加操作失败
//条件一:如果数组长度大于cpu数量 成立就表示扩容意向为 false;如果没有大于 就走下一个判断条件
//条件二:如果 cells != as 表示当前有其他线程已经 扩容过了,当前线程 rehash 过重试即可
else if (n >= NCPU || cells != as) {
//表示不扩容了
collide = false; //长度n是递增的,执行到了这个分支,说明n >= NCPU会永远为true,
//下面两个else if就永远不会被执行了,也就永远不会再进行扩容
} // At max size or stale
//collide 取反成立后 设置扩容意向 并不一定 真的 发生扩容
else if (!collide) {
//把扩容意向设置为true,只有这里才会给collide赋值为true,也只有执行了这一句,
//才可能执行后面一个else if进行扩容
collide = true;
}
//线程对应位置不为空 && 上一个if分支也执行过了 线程也重新执行rehash了 &&
//相应的新位置 cell 执行cas追加操作失败 && 执行扩容
//真正扩容的代码
//条件一:cellsBusy == 0 表示无锁状态,当前线程可以去竞争这把锁
// 成立:走下一个判断条件
// 不成立: 直接 走到代码最后 再次给当前线程重新 rehash 机会
//条件二:casCellsBusy() 并且 通过原子方式 获取锁
// 成立:走进代码内部
// 不成立:不会走到这一步
else if (cellsBusy == 0 && casCellsBusy()) {
try {
//检查下是否被别的线程扩容了(CAS更新锁标识,处理不了ABA问题,这里再检查一遍)
if (cells == as) {
//执行2倍扩容
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i) {
rs[i] = as[i];
}
//扩容完毕 内存可见性也设置了
cells = rs;
}
} finally {
//释放锁
cellsBusy = 0;
}
//回归扩容意向
collide = false;
// Retry with expanded table 扩容后重头再来
continue;
}
//重新给线程生成一个hash值,降低hash冲突,减少映射到同一个Cell导致CAS竞争的情况
h = advanceProbe(h);
}
// Cells 没有被初始化,并且也没有被加锁(被别的线程正在初始化)
//那么就尝试对它进行加锁,加锁成功进入这个else if
// CASE2 前置条件:cells 还未被初始化,需要被初始化
// 条件一:cellsBusy == 0 成立:说明 当前未加锁;
// 条件二: cells == as 再次对比 因为 害怕之前 多线程之前已经执行初始化了;
// 条件二: casCellsBusy() 表示获取锁成功 ;失败 表示其他锁持有这个锁;
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
// Initialize table
try {
// 又来对比 防止其他线程已经初始化了 当前线程又来进行初始化 造成数据丢失
if (cells == as) {//CAS避免不了ABA问题,这里再检测一次,
//如果还是null,或者空数组,那么就执行初始化
Cell[] rs = new Cell[2];//初始化时只创建两个单元
// h为线程的hash值, 对其中一个单元进行累积操作,另一个不管,继续为 null
rs[h & 1] = new Cell(x);
cells = rs;
//初始化结束
init = true;
}
} finally {
// 清空自旋标识,释放锁
cellsBusy = 0;
}
// 如果某个原本为null的Cell单元是由自己进行第一次累积操作,
//那么任务已经完成了,所以可以退出循环
if (init) {
//初始化完毕了 + 线程对应的 cell 值也 赋值好了 那就完事了 直接退出代码块
break;
}
}
//CASE3 :前提 上面 CASE1 把cells初始化过的处理了;
//CASE2 把cells初始化处理了; CASE3就是处理 初始化cells失败的兜底方案;
// 1. casCellsBusy() false -> 当前 CellsBusy 加锁状态 or casCellsBusy() 方法执行失败:
//表示其他线程正在初始化 cells 所以当前线程需要把数据累计到 base 中
// 2. cells == as false ->Cells 被其他线程初始化后 所以当前线程需要把数据累计到 base 中
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) {
//成功加到base后 就完事了 ; 没有加到的话 继续for循环
break;
} // Fall back on using base
}//for(;;) over
}//longAccumulate() over
总结:在并发情境下,为了保证共享变量的线程安全,首先提出了 synchronized 同步锁来保证并发线程安全,万事都有优点和缺点,优点是编程操作简单,缺点是最后的重量级锁线程之间的用户态和内核态的 切换耗费性能;然后就出现了CAS机制,不让线程去阻塞,而是采用自旋的方式来尝试线程安全,达到数据一致性;再后来的CAS机制还是有缺点,可能会导致大量的线程自旋消耗CPU性能,之后出现了资源均分概念,可以分摊热点,如此就能减少CPU消耗,以空间换时间,那么LongAdder对于未来还会有什么缺点,以及有什么新的理念来进行创新呢?