并发03:原子类

从 CAS 原理到 Atomic 系列,再到 LongAdder 的高并发优化,系统理解 Java 原子类的设计思想、适用场景和使用边界。

字数 1453 阅读时长 ≈ 5 分钟 2026-5-28 2026-7-12
并发03:原子类

原子类解决什么问题

很多并发问题第一次暴露,都是从 count++ 开始的。单线程里它很自然,多线程里却会丢数据,因为它不是一个动作,而是读值、加一、写回三个动作。

AtomicInteger 能把这类简单更新变成原子操作:

AtomicInteger count = new AtomicInteger();
count.incrementAndGet();

但它不是万能计数器,更不是业务一致性的万能锁。它解决的是 JVM 内存里一个变量的并发更新问题,不解决跨进程、不解决数据库状态、不解决”先检查再写入”的复杂业务规则。

CAS 原理

Atomic 系列的核心通常是 CAS:Compare And Swap。它的意思是,更新前先比较当前值是不是我期望的旧值,如果是,就改成新值;如果不是,说明别人改过,当前线程再重试或失败。

CAS 与 LongAdder 计数模型

可以用伪代码理解:

do {
    oldValue = value;
    newValue = oldValue + 1;
} while (!compareAndSet(oldValue, newValue));

这比互斥锁轻一些,因为线程没有进入阻塞等待,而是在失败后继续尝试。低冲突场景下,CAS 很高效;冲突很高时,大量线程反复失败重试,CPU 也会被消耗掉。

所以 Atomic 不是”没有成本”,它只是把等待方式从挂起排队变成乐观重试。

AtomicInteger 的适用场景

它适合保护单个变量的简单状态变化,比如:

  • 本机内存里的成功次数、失败次数
  • 当前 JVM 内的开关版本号
  • 简单的自增序列
  • 无需强一致实时读取的局部指标

如果更新逻辑只围绕一个变量,并且冲突不算极端,Atomic 系列会比加锁更直接。

但如果状态由多个字段共同决定,Atomic 就不够了。比如订单状态、库存数量和扣减记录要一起满足规则,这不是一个 AtomicInteger 能兜住的。

LongAdder 解决高冲突计数

高并发统计场景里,很多线程同时更新同一个 AtomicLong,CAS 冲突会很严重。LongAdder 的思路是把热点拆开:多个线程分散更新不同的 cell,读取时再汇总。

LongAdder qps = new LongAdder();
qps.increment();
long snapshot = qps.sum();
AtomicLongLongAdder
单个热点变量分散到多个 cell
写入随冲突增加而变慢写入吞吐更高
读取精确sum() 是快照,不保证瞬时精确
适合低冲突场景适合高冲突指标统计

它适合指标统计、QPS 计数、监控埋点这类场景。它的取舍是写入吞吐更高,但 sum() 读到的是汇总快照,不适合做需要严格实时精确的业务判断。

AtomicReference 保护对象引用

Atomic 不只用于数字。AtomicReference 可以原子替换对象引用,很适合配置快照、路由表、规则集这类”整体替换”的场景:

AtomicReference<RouteTable> tableRef = new AtomicReference<>(oldTable);
tableRef.set(newTable);

如果 RouteTable 是不可变对象,读线程拿到引用后就是一个稳定快照。如果 RouteTable 内部还有可变 Map,并且其他线程继续修改这个 Map,那么 AtomicReference 只能保证引用替换原子,不能保证内部结构安全。

更高级一点,可以用 updateAndGet 把读旧值、计算新值、CAS 更新封装起来:

ref.updateAndGet(old -> old.withNewRule(rule));

但更新函数要避免副作用。因为 CAS 失败后函数可能被重新执行,如果里面发消息、写数据库、扣库存,就可能重复产生副作用。

ABA 问题与版本号

CAS 还有一个经典问题叫 ABA:线程 A 看到值是 1,准备改成 2;期间线程 B 把值从 1 改成 3,又改回 1;线程 A 再 CAS 时发现还是 1,以为没人改过。

很多普通计数场景不受 ABA 影响,但引用、栈节点、对象状态流转可能会受影响。Java 里有 AtomicStampedReferenceAtomicMarkableReference 这类工具,通过版本或标记辅助判断变化。

项目里更常见的版本号,是数据库乐观锁:

update product
set stock = stock - 1, version = version + 1
where id = ? and stock > 0 and version = ?;

这和 CAS 思路很像:只有版本还是我读到的版本,才允许提交。

伪共享问题

高并发计数还有一个底层问题:伪共享。多个线程更新的变量如果落在同一个 CPU 缓存行,即使它们逻辑上互不相关,也可能因为缓存一致性协议互相影响。

LongAdder 通过分散 cell 降低热点竞争,也会尽量减少这类缓存行争用。我们不一定要手写填充字段,但要知道为什么”多个 AtomicLong 放在一起”在极端高并发下可能表现不好。

常见误区

误区一:用 Atomic 解决库存扣减

单机 demo 可以,真实服务多实例部署后,每个 JVM 里的 Atomic 只能管自己,管不了其他实例。共享库存还是要落到数据库、Redis 脚本、消息串行化或专门库存服务里。

误区二:以为 Atomic 操作组合起来仍然原子

两次 get() 和一次 compareAndSet() 之间,其他线程仍然可能插进来。复杂条件更新要放进一个 CAS 循环,或者换成锁。

误区三:把 LongAdder 用在强一致判断上

它强在分散热点,不强在瞬时精确。

项目里怎么选

可以按这个顺序判断:

  1. 状态是不是只在当前 JVM 内共享
  2. 更新是不是围绕单个变量
  3. 冲突高不高:CAS 失败重试是否会明显消耗 CPU?
  4. 读取是否要求瞬时精确
  5. 是否需要和数据库、缓存或消息里的状态一起保持一致

本地简单计数用 AtomicLong,高冲突指标用 LongAdder,多字段状态用锁或不可变快照,跨服务共享状态回到数据库约束、Redis 原子脚本或消息串行化。

一句话:Atomic 系列解决的是”一个内存变量怎么安全更新”,不是”一个业务状态怎么永远正确”。