并发场景里最常见的一类问题是:多个线程/多次回调同时更新同一份数据。你既不想上大锁把吞吐打崩,又必须保证正确性(尤其是"只处理一次"的幂等逻辑)。CAS(Compare-And-Swap / Compare-And-Set)就是为此准备的底层能力。
这篇文章讲三件事:
- CAS 是什么、原理是什么、为什么能无锁更新
- Java 里 CAS 怎么用(Atomic 系列)以及何时需要自旋
- 一个能直接线上用的例子:订单支付回调幂等 + 状态机(NEW→PAYING→PAID) ,避免重复执行副作用且防止"半成功"
1)CAS 是什么?
CAS 是一个原子操作,语义是:
如果内存中的值 == 期望值 expected,就更新为新值 new;否则失败。
伪代码:
typescript
boolean CAS(addr, expected, newValue) {
if (*addr == expected) {
*addr = newValue
return true
}
return false
}
CAS 的原子性来自 CPU 指令(例如 x86 的 cmpxchg),JVM 把它封装成 Java API 给我们用。
2)CAS 的典型写法:自旋重试(什么时候需要)
CAS 只返回成功/失败,不会像锁那样阻塞等待。所以你会看到两类用法:
2.1 必须最终成功:需要自旋(比如计数器 +1)
ini
while (true) {
int old = count.get();
int next = old + 1;
if (count.compareAndSet(old, next)) break;
}
这段循环的意思是:
如果我更新失败,说明被别人抢先改了,那我重新读最新值再算再试,直到成功为止 。
这种场景不自旋就会"丢更新"。
2.2 只允许执行一次:不需要自旋(幂等抢占执行权)
比如支付回调、发券、发货这种副作用逻辑,语义是:
只有一个线程能执行一次;其他线程发现已经处理过就直接返回。
这种场景就应该"一枪"CAS:
scss
if (!cas(NEW, PAYING)) return; // 抢不到就直接走幂等
doSideEffectsOnce();
失败了不要自旋,因为失败意味着"别人已经在处理/处理过",你不应该再抢。
3)CAS 为什么快?有什么坑?
优点
- 无阻塞:失败不挂线程,不进入 OS 锁
- 冲突低时吞吐高、延迟低
缺点
- 冲突高会导致自旋多,CPU 飙升(计数热点尤其明显)
- 经典问题:ABA(A→B→A),无锁数据结构里要用版本号解决(AtomicStampedReference)
4)Java 里 CAS 用在哪?
你日常其实一直在用:
AtomicInteger/AtomicLong/AtomicReference:核心就是compareAndSetLongAdder:高并发计数器,减少热点 CAS 冲突ConcurrentHashMap、AQS/ReentrantLock:关键路径都用 CAS
结论:CAS 是很多并发工具的地基。
5)线上可用例子:订单支付回调幂等(NEW→PAYING→PAID)
很多人用 CAS 写幂等,会直接 NEW → PAID,这在"纯状态"上没问题,但线上常见坑是:
- 你把状态改成 PAID 了
- 但"发券/记账/发MQ"等副作用执行到一半异常
- 结果变成:状态已终态,但副作用不完整(最难排查)
更稳的工程写法是加一个中间态:PAYING(处理中/抢占态):
NEW → PAYING:CAS 抢占执行权(只有一个线程能成功)- 做副作用(确保只执行一次)
- 成功后
PAYING → PAID - 失败则回滚为
NEW或标记PAY_FAILED(看业务)
这样能把"执行权抢占"和"副作用完成"解耦,线上更稳。
5.1 状态定义
swift
public enum OrderStatus {
NEW, // 未支付
PAYING, // 支付处理中(抢占执行权)
PAID, // 已支付
PAY_FAILED, // 支付处理失败(可选)
CANCELED
}
5.2 订单对象:AtomicReference 保存状态
arduino
import java.util.concurrent.atomic.AtomicReference;
public class Order {
private final String orderId;
private final AtomicReference<OrderStatus> status = new AtomicReference<>(OrderStatus.NEW);
public Order(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
public OrderStatus getStatus() {
return status.get();
}
public boolean casStatus(OrderStatus expected, OrderStatus update) {
return status.compareAndSet(expected, update);
}
}
5.3 支付回调处理器:抢占执行权 + 幂等 + 防半成功
scss
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PaymentCallbackHandler {
private static final Logger log = LoggerFactory.getLogger(PaymentCallbackHandler.class);
public void onPaid(Order order, String paymentNo) {
// 1) CAS 抢占执行权:只有一个线程能把 NEW 改成 PAYING
boolean won = order.casStatus(OrderStatus.NEW, OrderStatus.PAYING);
if (!won) {
// 幂等点:重复回调/并发处理都直接返回
log.info("ignore duplicate/parallel callback. orderId={}, status={}, paymentNo={}",
order.getOrderId(), order.getStatus(), paymentNo);
return;
}
// 2) 只有抢到执行权的线程能执行副作用
try {
log.info("won execution. start side effects. orderId={}, paymentNo={}",
order.getOrderId(), paymentNo);
doSideEffectsOnce(order, paymentNo);
// 3) 副作用完成后再推进终态
order.casStatus(OrderStatus.PAYING, OrderStatus.PAID);
log.info("paid success. orderId={}, finalStatus={}", order.getOrderId(), order.getStatus());
} catch (Exception e) {
// 4) 失败处理:可以回滚为 NEW 让后续重试,或标记失败态
order.casStatus(OrderStatus.PAYING, OrderStatus.PAY_FAILED);
log.warn("paid handling failed. orderId={}, finalStatus={}, err={}",
order.getOrderId(), order.getStatus(), e.toString());
// 线上通常还会:记录失败原因、报警、人工补偿/重试
}
}
private void doSideEffectsOnce(Order order, String paymentNo) {
// 示例:记账、发券、发消息、写流水......
// 这里要确保这些动作要么可幂等,要么保证只在"won"的线程执行
// 例如:写一条支付流水(唯一键 orderId),重复写会失败 -> 幂等
// 或者调用下游时带幂等号 paymentNo / requestId
// 模拟业务
if (paymentNo.endsWith("7")) {
// 模拟偶发异常,验证"PAYING -> PAY_FAILED"不会出现 PAID 半成功
throw new RuntimeException("simulate downstream failure");
}
}
}
这个方案解决了什么?
- 只执行一次:NEW→PAYING 的 CAS 只有一个线程能成功,其它线程直接返回
- 不自旋:抢不到就是幂等返回,不会烧 CPU
- 防半成功:状态只有在副作用完成后才推进到 PAID
- 失败可观测:PAY_FAILED 可以用于报警、补偿、重试策略
5.4 并发测试:证明只有一个线程能"抢到执行权"
ini
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CasOrderDemo {
public static void main(String[] args) throws Exception {
Order order = new Order("O20251221");
PaymentCallbackHandler handler = new PaymentCallbackHandler();
int n = 20;
ExecutorService pool = Executors.newFixedThreadPool(8);
CountDownLatch latch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
String paymentNo = "P" + i;
pool.submit(() -> {
try {
handler.onPaid(order, paymentNo);
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("final status: " + order.getStatus());
}
}
你会观察到:
- 只有一个线程打印 "won execution"
- 其它都是 "ignore duplicate/parallel callback"
- 如果你故意让 "副作用" 抛异常,最终状态会落在
PAY_FAILED而不是PAID
6)补一句真实线上:跨实例一致怎么办?
上面例子是 JVM 内存态,适合单实例内控制或中间态控制。真正订单最终要落库,跨实例一致常用数据库乐观锁(版本号):
ini
UPDATE orders
SET status='PAYING', version=version+1
WHERE order_id=? AND status='NEW' AND version=?;
影响行数=1 才算抢到执行权,0 表示被别人抢先处理/状态已变,直接幂等返回。
本质上这就是存储层的 CAS。
7)总结
- CAS 是"比较并交换"的原子操作,是无锁并发的基础
- 自旋 CAS 适用于"必须最终成功"的场景(计数器、累加)
- 幂等抢占执行权适用于"只允许执行一次"的副作用场景(支付回调、发券、发货)------不需要自旋
- 工程上更稳的做法是引入中间态:
NEW → PAYING → PAID,避免"状态终态但副作用半成功"