JUC从实战到源码:增强原子类高级应用与源码解析
😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人
🌝分享学习心得,欢迎指正,大家一起学习成长!
转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)
1 前言
上一篇文章《JUC从实战到源码:原子类全解析-从基础到应用》描述了 j.u.c 的原子类基本操作,接下来就需要进一步了解增强原子类与其源码。本章节从增强原子类的基本了解进行入手,包括前置知识,慢慢渗透到源码分析,源码分析通过 LongAdder 的源码来进行讲解。
2 增强原子类
首先我们看一下 j.u.c.atomic 包下的类:
上一节我们提到的原子类的基本操作类型是源自于 jdk1.5 的,接下来要学到的四个增强原子类是 jdk1.8,可见是有更强的功能与性能。
我们再来看阿里巴巴开发手册
如果是 jdk8,推荐使用 LondAdder 对象,原因是能够减少乐观锁的重试次数(后续通过讲解就能够了解为什么能够减少)。
增强类有DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder,这些都是 jdk8 的类,主要通过 LongAdder 来展开学习。
3 LongAdder 与 LongAccumulator
Java中的LongAdder和LongAccumulator是java.util.concurrent.atomic包中的两个类,它们专为高并发环境设计,用于高效地进行数值计算。这两个类旨在解决AtomicLong 等传统原子类在高并发场景下可能出现的性能瓶颈问题,提供更高的吞吐量和更好的性能。
3.1 LongAdder
在多线程进行更新的时候,通常是比 AtomicLong 更加优选使用,用于诸如收集统计信息,不用于细粒度同步控制的共同总合。在低竞争下,LongAdder 与 AtomicLong 性能相似,但是在高竞争下,LongAdder 有明显高的预期吞吐量,但是所消耗的的空间更高。
如下表常用 API
| 方法名 | 备注 |
|---|---|
| void add(long x) | 将当前值加上 x 值 |
| void increment() | 将当前值加 1 |
| void decrement() | 将当前值减 1 |
| long sum() | 返回当前值,在没有并发更新 value 的时候,sum 会返回一个精确值,但是如果是并发情况下是无法返回精确值。 |
| void reset() | 将 value 重置为 0,相当于重新 new LongAdder,只能在没有并发更新的情况下使用。 |
| long sumThenReset() | 获取当前的 value 并且重置 0,相当于 sum() + reset() |
首先我们先看一下 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;
}
这个 api 能够返回当前求和数据,但是他并不是一个原子性的快照。(Returns the current sum. The returned value is NOT an atomic snapshot;)
我们通过一个简单的例子来认识一下 longAdder 的 api。
public class LongAdderApiDemo {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());
}
}
increment能够将 longAdder 的值自增 1。
3.2 LongAccumulator
LongAccumulator是LongAdder的泛化版本,不仅限于求和,还支持通过自定义的二元操作符(LongBinaryOperator)实现各种类型的累积操作,如求最大值、最小值、乘积等。它比LongAdder更加灵活。
先看一下其构造方法:
public LongAccumulator(LongBinaryOperator accumulatorFunction,
long identity) {
this.function = accumulatorFunction;
base = this.identity = identity;
}
其中 LongBinaryOperator:二元操作符,是一个函数接口,定义累积逻辑。
@FunctionalInterface
public interface LongBinaryOperator {
/**
* Applies this operator to the given operands.
*
* @param left the first operand
* @param right the second operand
* @return the operator result
*/
long applyAsLong(long left, long right);
}
这个函数接口,里面含有两个参数,分别代表第一第二个操作数。
还有一个 identity:初始化值,表示函数初始的值。
例如以下例子:
public class LongAccumulatorDemo {
public static void main(String[] args) {
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 8);
longAccumulator.accumulate(10); // 18
longAccumulator.accumulate(10); // 28
System.out.println(longAccumulator.get());
}
}
相当于我们定义了一个加法运算,初始值是 8,当执行第一个 accumulate(10)时,x 的值就是 10,初始化值 8 也就是 y 的值,最终计算为 10 + 8 = 18;以此类推到第二个计算为 10 + 18 = 28。
例如以下例子求最大值:
LongAccumulator maxAccumulator = new LongAccumulator(Math::max, 12);
maxAccumulator.accumulate(100);
System.out.println(maxAccumulator.get());
函数中定义了 Math 中的求最大值,初始值是 12,经过accumulate 结果计算得到最大值是 100。
说个直白的话就是,先定义函数,这个函数可以是各种操作:加减乘除,求最大值等等,在通过
accumulate方法操作,就会将当前的值与操作属于要的值进行通过定义好的函数去计算。(这种函数方法的思想,在工作中能够做出许多通配的方法。)
3.3 二者对比
| 特性 | LongAdder | LongAccumulator |
|---|---|---|
| 主要用途 | 高效求和 | 灵活的累积操作 |
| 操作类型 | 仅支持加法 | 支持自定义二元操作 |
| 核心方法 | add(), sum() | accumulate(), get() |
| 灵活性 | 较低 | 较高 |
| 性能优化 | 分散竞争到Cell数组 | 分散竞争到Cell数组 |
4 原子类高性能的使用案例
接下来我们来通过统计计算的案例,来进行比较效率的高低。
通过 50 个线程,每个线程执行 50w 次加 1 操作,最后进行输出耗时和计算的结果值。
class ClickNum {
int number;
// 第一种:synchronized修饰
public synchronized void clickAddNumBySync() {
number++;
}
// 第二种:AtomicLong
AtomicLong atomicLong = new AtomicLong(0);
public void clickAddNumByAtomicLong() {
atomicLong.getAndIncrement();
}
// 第三种:LongAdder
LongAdder longAdder = new LongAdder();
public void clickAddNumByLongAdder() {
longAdder.increment();
}
// 第四种:LongAccumulator
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
public void clickAddNumByLongAccumulator() {
longAccumulator.accumulate(1);
}
}
public class Demo {
public static final int _1w = 10000;
public static final int threadNum = 50;
public static void main(String[] args) throws InterruptedException {
ClickNum clickNum = new ClickNum();
CountDownLatch count1 = new CountDownLatch(threadNum);
CountDownLatch count2 = new CountDownLatch(threadNum);
CountDownLatch count3 = new CountDownLatch(threadNum);
CountDownLatch count4 = new CountDownLatch(threadNum);
long startTime;
long endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1w; j++) {
clickNum.clickAddNumBySync();
}
} finally {
count1.countDown();
}
}, "T-" + i).start();
}
count1.await();
endTime = System.currentTimeMillis();
System.out.println("clickAddNumBySync 执行计算" + threadNum + "个线程,每次计算" + _1w + "次,耗时:" + (endTime - startTime) + "毫秒,计算结果:" + clickNum.number);
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1w; j++) {
clickNum.clickAddNumByAtomicLong();
}
} finally {
count2.countDown();
}
}, "T-" + i).start();
}
count2.await();
endTime = System.currentTimeMillis();
System.out.println("clickAddNumByAtomicLong 执行计算" + threadNum + "个线程,每次计算" + _1w + "次,耗时:" + (endTime - startTime) + "毫秒,计算结果:" + clickNum.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1w; j++) {
clickNum.clickAddNumByLongAdder();
}
} finally {
count3.countDown();
}
}, "T-" + i).start();
}
count3.await();
endTime = System.currentTimeMillis();
System.out.println("clickAddNumByLongAdder 执行计算" + threadNum + "个线程,每次计算" + _1w + "次,耗时:" + (endTime - startTime) + "毫秒,计算结果:" + clickNum.longAdder.sum());
startTime = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 100 * _1w; j++) {
clickNum.clickAddNumByLongAccumulator();
}
} finally {
count4.countDown();
}
}, "T-" + i).start();
}
count4.await();
endTime = System.currentTimeMillis();
System.out.println("clickAddNumByLongAccumulator 执行计算" + threadNum + "个线程,每次计算" + _1w + "次,耗时:" + (endTime - startTime) + "毫秒,计算结果:" + clickNum.longAccumulator.get());
}
}
运行结果
clickAddNumBySync 执行计算50个线程,每次计算10000次,耗时:1364毫秒,计算结果:50000000
clickAddNumByAtomicLong 执行计算50个线程,每次计算10000次,耗时:629毫秒,计算结果:50000000
clickAddNumByLongAdder 执行计算50个线程,每次计算10000次,耗时:118毫秒,计算结果:50000000
clickAddNumByLongAccumulator 执行计算50个线程,每次计算10000次,耗时:56毫秒,计算结果:50000000
由此可见,当在执行数据多的情况下,LongAccumulator的效果是最佳的,性能差距比较大,接下来就是学习一下底层原理。
5 源码分析
从上文的例子可以看出 LongAdder 与 LongAccumulator 效率是较高的,那么接下来就来了解一下底层原理。
5.1 前置知识
先来看一下这个类图来稍微了解一下代码的结构:
可见 LongAdder继承了 Striped64类,最终继承了 Number类,再看 AtomicInteger与 AtomicLong都是继承了Number类。可见最重要的是Striped64类与 Number类。
在上段落我们也提到了阿里巴巴开发手册中提到了推荐使用 LongAdder ,其性能比 AtomicLong 好(能够减少乐观锁次数)。在低竞争下,LongAdder 与 AtomicLong 性能相似,但是在高竞争下,LongAdder 有明显高的预期吞吐量,但是所消耗的的空间更高。
5.1.1 Striped64 类
首先我们要了解一下这个类,其中有个类(单元格类)
@sun.misc.Contended static final class Cell {...}
它是AtomicLong的填充变体,仅支持原始访问和CAS。
还有几个重要的参数:
/** CPU数 (cell数组的最大长度) */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* 单元格表。当非空时,size是2的幂(2,4,8,16...)方便做位运算。
*/
transient volatile Cell[] cells;
/**
* 基本值,当并发低时,只累加该值,主要用于没有竞争的情况。通过CAS更新。
*/
transient volatile long base;
/**
* 自旋锁(通过CAS锁定)在调整大小或创建单元格时使用。
* 创建或者扩容cells数组时使用的自旋锁变量调整单元格大小,创建时候使用的锁。
*/
transient volatile int cellsBusy;
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 值。
5.1.2 Cell 类
Cell 类是 Striped64 类中的一个静态内部类
@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;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
5.2 原理说明
对于 AtomicLong,它是通过不断地自旋,进行 CAS 操作计算,而对于 LongAdder 确是进行了分散热点,通过分散到各个 Cell。
LongAdder 的基本思路就是分散热点,将 value 值分散到一个 Cell 数组中,不同线程会命中到数组中的不同槽位中,各个线程只对自己槽中的那个值进行 CAS 操作。将热点分散,冲突的概率也就大大降低,如果需要获取最终的结果,那就需要将槽中的值与 base 累加求和。
LongAdder 在无竞争的情况,跟 AtomicLong 一样,对同一个 base 进行操作,当出现竞争关系时是采用了分散热点的做法,通过空间换时间,用一个数组 cells,将 value 的值分散记录到 cells 数组中。多线程需要同时对 value 进行操作时候,可以对线程 id 进行 hash 得到 hash 值,在根据这个 hash 值映射到数组 cells 对应的下标,在对这个下标对应值进行自增操作。当所有线程执行结束后对 base 与 cells 数组进行求和。
5.3 add 源码解析
当调用 LongAdder#increment 方法,其也是执行了 add(1) 在通过 Striped64 与 Cell 的 CAS 操作,时序图如下:
add 代码如下:
public void add(long x) {
// as: cells引用
Cell[] as;
// b: 获取的base值,v:期望值
long b, v;
// m: cells数组长度
int m;
// a: 当前线程命中的cell单元格
Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
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);
}
}
首先需要对其几个变量进行一个了解
- as: cells引用
- b: 获取的base值
- v:期望值
- m: cells数组长度
- a: 当前线程命中的cell单元格
① 当执行 add 方法时,如果没有初始化 cells 数组,并且对 base 进行 CAS 操作+1 成功,则就是一个线程的执行操作,直接对 base 进行加 1。
② 当存在并发的时候,一开始也是没有对 cells 数组进行初始化,所以 (as = cells) != null还是 false,但是高并发的时候,对 base 进行 CAS 操作就会返回 false,这时候就会进入 if 条件语句。
③ 进入第一个 if 语句中,会判断 uncontended(没有冲突)的值,默认 true,当任何有一个满足第二个 if 语句,就会调用 longAccumulate方法,进行初始化 cells 数组。
对于 longAccumulate方法有多种操作(初始化 cells、扩容等),这个类里面有个死循环,里面做了许多操作。
当 cells 数组为空的时候,会走到如下:
// 初始化数组
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
// 因为是2的幂次,首次初始化固定为2
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
里面涉及的操作,首先是创建大小为 2 的 Cell 数组,并且根据计算出来的 hash 值作为下标,并且将cell对象初始化x值,放到 Cell 数组的对应槽位中。
④ 当再次并发的时候,到达 a = as[getProbe() & m]) == null(这个判断是通过getProbe() & m 计算 hash 值来获取 cell 的下标,并且判断这个位置是否是没值的),假设此时得到的 cell 的值不是 null,那么此时会进行 !(uncontended = a.cas(v = a.value, v + x))CAS 操作,如果在并发情况 CAS 操作返回 false 表示 CAS 失败,那么接下来还是进入 longAccumulate(x, null, uncontended);,只不过这次进入的操作是扩容操作。
⑤ longAccumulate 方法参数含义
- long x:需要增加的值,默认是 1
- LongBinaryOperator fn:操作函数,默认是 null
- boolean wasUncontended:竞争标识,只有是 false 的时候才是有竞争(只有在 cells 初始化之后,并且当前线程 CAS 失败,才会是 false)
5.4 longAccumulate 源码解析
5.4.1 分配 hash 值
首先会给当前线程分配一个 hash 值,一开始当得到 0 的时候,表示没有初始化,会进行强制初始化获取 hash 值。
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// 存储线程的getProbe值
int h;
// getProbe的值如果是0就表示随机数还没初始化
if ((h = getProbe()) == 0) {
// 强制初始化,为当前线程重新计算一个hash值
ThreadLocalRandom.current(); // force initialization
// 重新获取probe值,hash值被重置就好比一个新线程
h = getProbe();
// 重新计算当前线程的hash值后认为此次不是一次竞争(没有初始化,所以没有竞争)
wasUncontended = true;
}
// ...
}
getProbe(),在Striped64类的静态初始化块中,通过Unsafe获取的 threadLocalRandomProbe 字段是线程(Thread对象)内部的一个探针值(Probe),它用于哈希算法中为线程分配cells数组的索引,从而减少多线程竞争。
一开始如果 getProbe() 得到的是 0,就表示随机没有初始化,这时候会强制初始化一个新的线程,重新获取,就能够得到一个新的 hash 值,最后标记没有竞争。
5.4.2 自旋操作
最关键的 for 自旋中的操作分为了三段:cell 数组已经初始化的操作、cell 进行初始化、cell 正在初始化,尝试 base 操作(多个线程情况下,可能存在有线程正在初始化 cell)。
for (;;) {
Cell[] as; Cell a; int n; long v;
// cell数组已经初始化
if ((as = cells) != null && (n = as.length) > 0) {...}
// cell为null,进行初始化
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {...}
// cell正在初始化,尝试在base上进行操作
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))){...}
}
5.4.3 初始化 cell 数组
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
// 因为是2的幂次,首次初始化固定为2
Cell[] rs = new Cell[2];
// 根据前面计算的hash值进行位运算得到下标,并且将cell对象初始化x值
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
// 释放锁
cellsBusy = 0;
}
if (init)
break;
}
这段代码高并发累加设计的基石,通过 CAS 锁 + 惰性初始化 + 哈希分散 实现了高效的竞争处理。执行逻辑:
条件检查
cellsBusy == 0:表示当前没有线程在操作cells数组(cellsBusy是锁标志,0=无锁,1=锁定)。cells == as:确保在进入代码块前cells数组未被其他线程修改(as是之前的cell引用)。casCellsBusy():通过 CAS 操作将cellsBusy从 0 改为 1(获取锁),确保原子性。
双重检查:在 try 块内再次检查 cells == as,防止其他线程已修改 cells 导致重复初始化。
初始化数组
- 创建 cell 数组(长度选 2 是因后续扩容按 2 的幂次进行,便于位运算计算索引)。
- 通过
h & 1计算初始索引(h 是线程的探针哈希值),将新 Cell 放入对应槽位并赋初值 x。(h & 1 类似 HashMap 计算散列桶 index 的算法,通常是 hash & (table.len - 1))
将新的数组赋值给 cells 后标记 init=true 表示初始化成功。
在 finally 中释放锁,最后初始化成功后退出自旋。
5.4.5 兜底操作
当cells 数组正在初始化或扩容时,尝试直接通过 CAS 操作更新 base 字段的回退逻辑。
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break; // 回退到 base 操作
当cells 数组未初始化或正在被其他线程初始化(如 cellsBusy 被占用),或者线程无法通过 cells 数组完成更新(如哈希冲突导致多次 CAS 失败)。就会走到这个分支。
尝试 CAS 更新 base
- 获取当前 base 值:v = base。
- 计算新值:
- 如果 fn == null(默认累加操作),新值为 v + x。
- 如果存在自定义函数 fn(如 LongAccumulator 的累积操作),新值为 fn.applyAsLong(v, x)。
- 原子更新:通过 casBase 尝试将 base 从 v 更新为计算后的新值。
5.4.6 存在 cells 数组与扩容
这部分是最主要的核心部分,在 cells 数组已存在但线程竞争激烈时,通过动态扩容、重哈希和分散线程访问来优化性能。
if ((as = cells) != null && (n = as.length) > 0) {
// 分支1:当前线程的Cell槽位为空,尝试创建新Cell
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // 无锁状态,尝试创建Cell
Cell r = new Cell(x); // 乐观创建Cell对象
if (cellsBusy == 0 && casCellsBusy()) { // 获取锁(CAS设置cellsBusy=1)
boolean created = false;
try {
// 双重检查:确认槽位仍为空后插入新Cell
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; // 标记无冲突
}
// 分支2:上一次CAS失败,标记为“有竞争”并重试
else if (!wasUncontended) {
wasUncontended = true; // 后续操作需要重新哈希
}
// 分支3:尝试CAS更新当前Cell的值
else if (a.cas(v = a.value, (fn == null) ? v + x : fn.applyAsLong(v, x))) {
break; // 更新成功,退出循环
}
// 分支4:判断是否需要停止扩容(达到CPU核心数或数组已更新)
else if (n >= NCPU || cells != as) {
collide = false; // 不再尝试扩容
}
// 分支5:标记冲突,下次循环可能触发扩容
else if (!collide) {
collide = true; // 下次循环尝试扩容
}
// 分支6:执行扩容(cells数组翻倍)
else if (cellsBusy == 0 && casCellsBusy()) { // 获取锁
try {
if (cells == as) { // 双重检查数组未被修改
Cell[] rs = new Cell[n << 1]; // 新数组长度为原2倍
for (int i = 0; i < n; ++i) // 复制旧数组元素
rs[i] = as[i];
cells = rs; // 更新cells引用
}
} finally {
cellsBusy = 0; // 释放锁
}
collide = false; // 重置冲突标记
continue; // 扩容后重试
}
// 调整探针值,改变后续哈希计算
h = advanceProbe(h); // 重新哈希,减少未来冲突
}
来自 DeepSeek 分析
接下来我们对这部分的 if 进行拆分解读
① 当前 hash 所对应的槽位是 null
// (n - 1) & h 获取了当前线程的hash值,判断cell数组中对应下标的cell单元格是不是为null,是的话就是该cell没被使用
if ((a = as[(n - 1) & h]) == null) {
// cell数组没有正在扩容,cellsBusy判断是否有锁
if (cellsBusy == 0) { // Try to attach new Cell
// 创建一个单元格,初始值为传进来的x
Cell r = new Cell(x); // Optimistically create
// 进行CAS操作尝试加锁,加锁成功cellsBusy = 1
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
// 重复检测,确认槽位仍为空后插入新Cell
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;
}
// ...
// 如果没有创建成功
h = advanceProbe(h);
获取当前线程的 hash 值,判断其所在的 cell 数组的位置是否为 null,如果是则会进入上段代码的逻辑。(这也就意味着当前槽位没有被使用)如果没有创建新的 cell 对象,则会执行 h = advanceProbe(h);修改线程的 probe,继续循环尝试。
乐观创建新 Cell 对象。通过 cellsBusy CAS 获取锁,确保原子性修改 cells 数组。进行双重检查,在锁内再次确认槽位为空后插入新 Cell,避免其他线程已修改。若成功插入,退出循环;否则继续尝试其他策略。
② 处理 CAS 失败
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// ...
h = advanceProbe(h);
如果在 add 中 uncontended = a.cas(v = a.value, v + x)进行 CAS 操作失败,则会将竞争设置为 true。
标记
wasUncontended = true,后续操作将重新计算哈希值(advanceProbe),重新循环。
③ 重试CAS更新
// 尝试CAS更新当前Cell的值
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
说明当前线程对应的数组有数据,也重置过了 hash 值。这时通过 CAS 操作尝试对当前数中的 value 值进行累加 x 操作,x 默认是 1,如果 CAS 操作成功,则退出循环。
直接尝试 CAS 更新当前 Cell 的值。若成功,退出循环。
④ 判断扩容限制
// 判断是否需要停止扩容(达到CPU核心数或数组已更新)
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// ...
h = advanceProbe(h);
如果 n(cell 数组长度)大于 CPU 最大数量,不可扩容,将 collide(扩容期望)设置 false,并通过 h = advanceProbe(h);修改线程的 probe 在重新尝试。
数组长度已达 CPU 核心数(NCPU)或数组已被其他线程修改(cells != as),标记 collide = false,不再尝试扩容(避免无意义扩容或操作过期数组)。
⑤ 标记扩容
// 标记冲突,下次循环可能触发扩容
else if (!collide)
collide = true;
// ...
h = advanceProbe(h);
如果扩容意向 collidefalse,则会修改成 true,然后重新获取 hash 值继续循环。如果当前数组的长度大雨了 CPU 核数,则会再次设置 collide为 false。
⑥ 执行扩容
// 分支6:执行扩容(cells数组翻倍)
else if (cellsBusy == 0 && casCellsBusy()) { // 获取锁
try {
if (cells == as) { // 双重检查数组未被修改
Cell[] rs = new Cell[n << 1]; // 新数组长度为原2倍
for (int i = 0; i < n; ++i) // 复制旧数组元素
rs[i] = as[i];
cells = rs; // 更新cells引用
}
} finally {
cellsBusy = 0; // 释放锁
}
collide = false; // 重置冲突标记
continue; // 扩容后重试
}
h = advanceProbe(h);
获取锁之后,会进行双重检查数组未被修改(cells == as 表示当前的 cells 数组和最先赋值的 as 是同一个,代表没有被其他线程扩容过),扩容将会向左位移 1 位,将大小扩充原来的两倍。最后再将之前的数组的元素拷贝到新的数组中,最后释放锁、重置冲突标记,继续循环重新计算 hash 值继续操作。
5.5 sum 源码解析
sum 方法用于计算当前所有分散单元的累加值,但它是最终一致性(非原子快照)。sum()会将所有Cell数组中的value和base累加作为返回值。
public long sum() {
Cell[] as = cells; // 步骤1:获取cells数组的快照
long sum = base; // 步骤2:初始化总和为基准值
if (as != null) { // 步骤3:检查是否初始化了cells数组
for (int i = 0; i < as.length; ++i) { // 步骤4:遍历所有Cell单元
Cell a = as[i];
if (a != null) // 步骤5:过滤空槽位
sum += a.value; // 步骤6:累加单元值
}
}
return sum; // 步骤7:返回最终总和
}
sum 在执行的时候,没有对 cell 和 base 进行限制。
首先,最终返回的 sum 局部变量,初始被复制为base,而最终返回时,很可能 base 已经被更新了,而此时局部变量 sum 不会更新,造成不一致。其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum 方法在没有并发的情况下,可以获得正确的结果。
6 对比 AtomicLong
- AtomicLong:提供原子操作的长整型变量,通过CAS(Compare-And-Swap)实现无锁线程安全,适用于需要精确实时值的场景。保证精度,性能代价高。
- LongAdder:针对高并发写入场景优化,采用分段累加策略(分散竞争),牺牲读取的实时性以换取更高的写入吞吐量。不保证精度,但保证了性能。
总结如下表:
| 维度 | AtomicLong | LongAdder | 说明 |
|---|---|---|---|
| 性能 | 在低并发场景下性能尚可,但在高并发下由于频繁的 CAS 竞争,性能会急剧下降 | 在高并发场景下,通过分段累加策略分散竞争,写入吞吐量远高于 AtomicLong | 性能差异在并发程度升高时愈发显著 |
| 精度 | 提供精确的实时值,任何读取操作都能获取当前的准确数值 | 不能保证读取的实时性与精确性,sum() 方法返回的值可能存在延迟,仅能保证最终一致性 | 对精度要求高的场景慎用 LongAdder |
| 适用场景 | 适用于需要频繁读取精确值、写操作相对较少的场景,如银行账户余额的原子操作 | 适用于写操作远多于读操作、对实时性要求不高的高并发场景,如网站访问量统计 | 根据业务场景中读写操作的比例和对数据精度的要求选择合适的类 |
| 内存占用 | 只需维护一个单一的变量值,内存占用较低 | 需要维护一个 Cell[] 数组,在高并发下数组可能不断扩容,会占用更多内存 | 内存占用的差异在 Cell 数组较大时才明显 |
| 扩展性 | 功能单一,仅支持原子性的加减操作 | 可通过自定义累积函数实现更多操作,如求最大值、最小值等 | LongAccumulator 的灵活性使其适用范围更广 |
7 总结
在高并发场景下,LongAdder 和 LongAccumulator 通过将热点数据分散到多个 Cell 中,大幅降低了线程间的竞争,从而显著提升了写操作的吞吐量。它们适用于对最终结果精度要求不高、更注重系统吞吐量和响应时间的场景,如统计计数、监控指标收集等。而 DoubleAccumulator 和 DoubleAdder 则在原理上与 LongAdder 和 LongAccumulator 相同,只是将操作对象从长整型换成了双精度浮点型,适用于涉及浮点数的高并发统计场景。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!