并发03:原子类
从 CAS 原理到 Atomic 系列,再到 LongAdder 的高并发优化,系统理解 Java 原子类的设计思想、适用场景和使用边界。
原子类解决什么问题
很多并发问题第一次暴露,都是从 count++ 开始的。单线程里它很自然,多线程里却会丢数据,因为它不是一个动作,而是读值、加一、写回三个动作。
AtomicInteger 能把这类简单更新变成原子操作:
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
但它不是万能计数器,更不是业务一致性的万能锁。它解决的是 JVM 内存里一个变量的并发更新问题,不解决跨进程、不解决数据库状态、不解决”先检查再写入”的复杂业务规则。
CAS 原理
Atomic 系列的核心通常是 CAS:Compare And Swap。它的意思是,更新前先比较当前值是不是我期望的旧值,如果是,就改成新值;如果不是,说明别人改过,当前线程再重试或失败。
可以用伪代码理解:
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();
| AtomicLong | LongAdder |
|---|---|
| 单个热点变量 | 分散到多个 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 里有 AtomicStampedReference、AtomicMarkableReference 这类工具,通过版本或标记辅助判断变化。
项目里更常见的版本号,是数据库乐观锁:
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 用在强一致判断上
它强在分散热点,不强在瞬时精确。
项目里怎么选
可以按这个顺序判断:
- 状态是不是只在当前 JVM 内共享?
- 更新是不是围绕单个变量?
- 冲突高不高:CAS 失败重试是否会明显消耗 CPU?
- 读取是否要求瞬时精确?
- 是否需要和数据库、缓存或消息里的状态一起保持一致?
本地简单计数用 AtomicLong,高冲突指标用 LongAdder,多字段状态用锁或不可变快照,跨服务共享状态回到数据库约束、Redis 原子脚本或消息串行化。
一句话:Atomic 系列解决的是”一个内存变量怎么安全更新”,不是”一个业务状态怎么永远正确”。