Skip to content

第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();   // 原子自增,无需 synchronized

2. 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 / 选择

特性CJava
原子操作gcc __sync / C11 atomicAtomic 类
CAScmpxchg 内联汇编compareAndSet
无锁数据结构手写部分封装(ConcurrentLinkedQueue)

选择

  • 单变量原子 → AtomicInteger/Long
  • 高并发计数 → LongAdder
  • 对象引用 → AtomicReference
  • ABA 风险 → AtomicStampedReference
  • 复合操作/代码块 → synchronized/Lock

💡 九、最佳实践

  1. 单变量原子优先原子类,无锁更快。
  2. 高并发计数用 LongAdder,吞吐量远超 AtomicLong。
  3. 值复用场景防 ABA,用带版本号的 StampedReference。
  4. 原子类只保证单变量,复合操作用锁。
  5. CAS 失败重试在高竞争下放大,必要时回退到锁。

📝 练习预告

完成 练习/Ex28_AtomicClass.java 中的 6 道题:

  1. AtomicInteger 基本操作
  2. compareAndSet 原子条件更新
  3. AtomicReference 引用更新
  4. ABA 问题演示与解决
  5. LongAdder vs AtomicLong 性能
  6. 综合:无锁计数器实现

完成后对比 答案/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 只表达状态切换,不提供等待其他线程初始化完成的能力。

如果其他线程必须等待初始化完成,可以考虑 CountDownLatchFutureTask 或显式状态机。


🎓 下一步

  • 第29课:CompletableFuture — 异步编程、链式组合、异常处理