Appearance
第28课:原子类
🎯 学习目标
- 理解 CAS(Compare-And-Swap)原理与原子类的无锁机制
- 掌握 AtomicInteger/Long/Boolean、AtomicReference
- 理解 ABA 问题与 AtomicStampedReference 解决
- 掌握 LongAdder(高并发计数优于 AtomicLong)
- 了解 FieldUpdater、累加器类
📖 一、概念讲解:CAS 无锁
1. 原子类是什么
java.util.concurrent.atomic 包提供基于 CAS 的原子操作类,无需加锁即可保证单个变量的原子性。
java
AtomicInteger ai = new AtomicInteger(0);
ai.incrementAndGet(); // 原子自增,无需 synchronized2. CAS 原理
CAS(Compare-And-Swap):硬件级原子指令(x86 的 cmpxchg)。语义:
比较内存值 V 与期望值 A:
若 V == A,则把内存值设为新值 B,返回 true(成功)
若 V != A,说明已被其他线程改过,返回 false(失败,重试)java
// 自旋 CAS(原子类内部)
public final int getAndIncrement() {
int old;
do {
old = value; // 读当前值
} while (!compareAndSet(old, old + 1)); // 失败就重试
return old;
}3. CAS vs 锁
- CAS 无锁:不阻塞线程、无上下文切换、无死锁。单变量高并发最快。
- 锁(synchronized/Lock):互斥,适合复合操作/代码块。
- CAS 失败时自旋重试,高竞争下重试多(CPU 空转),此时锁可能更好。
📖 二、基本原子类
java
AtomicInteger ai = new AtomicInteger(10);
ai.get(); // 10
ai.set(20); // 直接设(无 CAS,仅保证可见性)
ai.getAndIncrement(); // 20,返回旧值
ai.incrementAndGet(); // 21,返回新值
ai.getAndAdd(5); // 21,返回旧值
ai.addAndGet(5); // 26,返回新值
ai.compareAndSet(26, 0); // true(当前26==期望26,设为0)
ai.compareAndSet(26, 1); // false(当前已0≠26)
AtomicBoolean ab = new AtomicBoolean(false);
ab.compareAndSet(false, true); // 原子"当为false时设为true"(一次初始化)
AtomicLong al = new AtomicLong(0); // 同 AtomicInteger,long 版📖 三、AtomicReference
原子引用,泛型版:
java
AtomicReference<String> ref = new AtomicReference<>("init");
ref.compareAndSet("init", "updated"); // 期望"init",匹配则设"updated"
String old = ref.getAndSet("new"); // 原子设值并返回旧值用途:无锁更新对象引用(如无锁栈、状态机)。注意只保证引用的原子性,不保证对象内部字段的原子性。
📖 四、ABA 问题
问题
CAS 只比较值,值从 A→B→A 时,CAS 认为没变(实际变过):
java
// 线程1读到 A,准备 CAS(A, C)
// 线程2把 A→B→A(值又变回 A)
// 线程1的 CAS(A,C) 成功,但它不知道中间发生过变化栈场景:弹出 A 后又压入新 A,CAS 误以为栈没变,可能破坏结构。
解决:AtomicStampedReference
加版本号,值+版本号一起比较:
java
AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 0);
int[] stamp = new int[1];
String val = asr.get(stamp); // val="A", stamp[0]=0
asr.compareAndSet("A", "B", 0, 1); // 期望值A+版本0,设值B+版本1
// 即便值变回 A,版本号递增,CAS 不再匹配📖 五、LongAdder(高并发计数)
JDK 8 引入,高并发计数远优于 AtomicLong:
java
LongAdder la = new LongAdder();
la.increment(); // 不一定更新主值,可能更新某个 Cell
la.add(5);
long sum = la.sum(); // 求和:主值 + 所有 Cell原理
AtomicLong 所有线程 CAS 同一个 value,高竞争下重试多。 LongAdder 把 value 拆成 base + 多个 Cell[],不同线程 CAS 不同 Cell,最后 sum 累加。竞争分散,吞吐量提升。
适用
- 高并发计数(统计、计数器)→ LongAdder
- 低竞争 → AtomicLong(开销更小)
- 需要精确单个值/比较 → AtomicLong(LongAdder 的 sum 非强一致瞬时值)
类似:LongAccumulator(自定义累加函数)、DoubleAdder。
📖 六、FieldUpdater
原子更新对象的 volatile 字段(避免为每个字段创建 Atomic 对象):
java
class Node {
volatile Node next;
private static final AtomicReferenceFieldUpdater<Node, Node> NEXT =
AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");
// NEXT.compareAndSet(this, oldNext, newNext)
}用途:JDK 并发数据结构(ConcurrentLinkedQueue 等)内部用,节省内存。普通业务少用。
⚠️ 七、常见陷阱
陷阱1:以为 CAS 适合所有场景
高竞争下 CAS 自旋开销大(CPU 空转),可能比锁慢。竞争激烈用锁或 LongAdder。
陷阱2:ABA 问题
栈/链表等场景值复用会引发 ABA。用 AtomicStampedReference 加版本号。
陷阱3:AtomicReference 不保证对象内部原子
java
AtomicReference<List> ref = new AtomicReference<>(list);
// ref 保证 list 引用原子,但 list.add 并发仍不安全陷阱4:LongAdder.sum 不精确
sum 是 base + 各 Cell 累加,并发下求和瞬间可能变化,非强一致。需要精确瞬时值用 AtomicLong。
陷阱5:用 Atomic 当锁
AtomicBoolean 做互斥(CAS false→true)能模拟锁,但无重入、无 Condition,复杂场景用 Lock。
🆚 八、Java vs C / 选择
| 特性 | C | Java |
|---|---|---|
| 原子操作 | gcc __sync / C11 atomic | Atomic 类 |
| CAS | cmpxchg 内联汇编 | compareAndSet |
| 无锁数据结构 | 手写 | 部分封装(ConcurrentLinkedQueue) |
选择:
- 单变量原子 → AtomicInteger/Long
- 高并发计数 → LongAdder
- 对象引用 → AtomicReference
- ABA 风险 → AtomicStampedReference
- 复合操作/代码块 → synchronized/Lock
💡 九、最佳实践
- 单变量原子优先原子类,无锁更快。
- 高并发计数用 LongAdder,吞吐量远超 AtomicLong。
- 值复用场景防 ABA,用带版本号的 StampedReference。
- 原子类只保证单变量,复合操作用锁。
- CAS 失败重试在高竞争下放大,必要时回退到锁。
📝 练习预告
完成 练习/Ex28_AtomicClass.java 中的 6 道题:
- AtomicInteger 基本操作
- compareAndSet 原子条件更新
- AtomicReference 引用更新
- ABA 问题演示与解决
- LongAdder vs AtomicLong 性能
- 综合:无锁计数器实现
完成后对比 答案/Sol28.java,查看逐行讲解与多解法。
📖 十、CAS 的内存语义
CAS 不只是比较和替换,它还涉及内存可见性。
原子类内部的值通常是 volatile 字段:
text
volatile 保证读取到最新值。
CAS 保证更新是原子的。
失败后重试保证不会覆盖其他线程更新。因此:
text
AtomicInteger.incrementAndGet 是原子自增。
普通 volatile int 的 count++ 不是原子操作。volatile 解决可见性,Atomic 解决单变量原子性。
📖 十一、函数式更新方法
原子类提供函数式更新:
java
AtomicInteger value = new AtomicInteger(10);
value.updateAndGet(x -> x * 2);
value.getAndUpdate(x -> x + 1);
value.accumulateAndGet(5, Integer::max);区别:
text
updateAndGet:返回更新后的值。
getAndUpdate:返回更新前的值。
accumulateAndGet:把当前值和给定值合并。注意:
text
函数可能在 CAS 失败后被重复调用。
函数必须无副作用。
不要在函数里写日志、发消息、修改外部状态。🧪 十二、AtomicReference 不可变更新
AtomicReference 更适合配合不可变对象。
java
record Config(String host, int port) {}
AtomicReference<Config> ref = new AtomicReference<>(new Config("localhost", 8080));
public void updatePort(int newPort) {
ref.updateAndGet(old -> new Config(old.host(), newPort));
}优势:
text
引用替换是原子的。
旧对象不可变,不会被并发修改。
读线程总能看到一个完整配置。不要这样理解:
text
AtomicReference<List<T>> 只保证 List 引用替换原子。
它不保证 list.add 本身线程安全。📖 十三、LongAdder 的限制
LongAdder 适合高并发统计,但不是 AtomicLong 的完全替代。
适合:
text
QPS 计数。
请求总数。
命中次数。
监控指标累加。不适合:
text
需要 compareAndSet 的状态更新。
需要读取瞬时精确值后立即决策。
需要和其他变量保持强一致。sum() 不是线性一致快照。在并发更新时,它只是一个统计视图。
📖 十四、伪共享与高并发计数
高并发计数中可能出现伪共享。
概念:
text
CPU 缓存以 cache line 为单位加载。
多个热点变量如果落在同一 cache line。
不同核心频繁修改它们,会导致缓存行反复失效。LongAdder 通过分散到多个 Cell 降低竞争,也间接缓解热点写同一个变量的问题。
业务层不必一开始就手写填充字段,但要知道:
text
AtomicLong 高竞争下可能 CAS 失败很多。
LongAdder 用空间换吞吐。
监控计数优先 LongAdder。
强一致状态更新优先 AtomicLong 或锁。🛠 十五、原子类排查清单
常见问题:
text
把 AtomicReference 里的对象当成线程安全对象。
在 update 函数里做有副作用操作。
高竞争下 AtomicLong CPU 飙高。
需要复合原子性却只用了多个 Atomic 变量。
忽略 ABA。
LongAdder.sum 被当成强一致值。
AtomicBoolean 自旋锁没有超时和重入能力。判断是否该用原子类:
text
是否只保护一个变量。
是否可以用 CAS 重试表达更新。
是否能接受高竞争下自旋成本。
是否不需要条件等待。
是否不需要跨多个变量的一致性。✅ 十六、掌握标准
学完本课后,应能做到:
text
能解释 CAS 的比较、替换和重试。
能区分 volatile 和 Atomic 的职责。
能使用 AtomicInteger/AtomicLong 做单变量更新。
能使用 AtomicReference 做不可变对象引用替换。
能解释 ABA 问题和版本号解决思路。
能判断 AtomicLong 与 LongAdder 的适用场景。
能避免在 update 函数中写副作用。
能知道复合操作仍然需要锁或更高层抽象。原子类适合把一个小状态更新做成无锁操作。只要问题变成多个状态的一致性,优先重新考虑锁、队列或不可变设计。
🧪 十七、案例:一次性初始化开关
AtomicBoolean 常用于保证某段初始化逻辑只执行一次。
java
class Initializer {
private final AtomicBoolean initialized = new AtomicBoolean(false);
public void init() {
if (!initialized.compareAndSet(false, true)) {
return;
}
doInit();
}
}语义:
text
第一个线程把 false 改成 true,执行初始化。
后续线程 CAS 失败,直接返回。
整个过程不需要 synchronized。注意:
text
如果 doInit 失败,是否要把 initialized 改回 false,需要按业务决定。
如果初始化过程很复杂,锁可能比 AtomicBoolean 更容易表达错误恢复。
AtomicBoolean 只表达状态切换,不提供等待其他线程初始化完成的能力。如果其他线程必须等待初始化完成,可以考虑 CountDownLatch、FutureTask 或显式状态机。
🎓 下一步
- 第29课:CompletableFuture — 异步编程、链式组合、异常处理