mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1628 words
4 minutes
Java CAS 专题:从 AtomicInteger 到乐观并发
2025-04-02

Java CAS 专题:从 AtomicInteger 到乐观并发#

CAS 是 Java 并发里非常核心的概念。

很多并发工具底层都会看到它的影子,比如:

  • AtomicInteger
  • AtomicLong
  • LongAdder
  • ConcurrentHashMap
  • AQS;
  • 乐观锁思想。

这篇把 CAS 从主线文章里单独拆出来讲,避免并发系列前几篇塞得太满。

主线文章可以先看:

锁相关内容看这里:Java 锁专题:从 synchronized 到分布式锁

CAS 是什么#

CAS 全称是 Compare And Swap,比较并交换。

它的核心思想是:

我认为当前值是 A
如果内存里的值现在还是 A
那就把它改成 B
如果已经不是 A
说明别人改过了,这次更新失败

换成代码里的概念就是:

compareAndSet(expectedValue, newValue)

只有当前值等于期望值时,才会更新成功。

为什么需要 CAS#

先看一个普通计数器:

public class Counter {
private int count = 0;
public void add() {
count++;
}
}

count++ 不是原子操作,它包含三个步骤:

读取 count
加 1
写回 count

多个线程同时执行时,可能出现丢失更新。

使用 synchronized 可以解决,但它会让线程进入互斥等待。

CAS 的思路是不先加锁,而是更新时检查:

如果值没有被别人改过,我就更新;如果被改过,我就重试。

AtomicInteger#

Java 里最常见的 CAS 工具是 AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet();
}
public int get() {
return count.get();
}
}

incrementAndGet() 底层会不断尝试 CAS。

大致逻辑可以理解成:

while (true) {
int oldValue = get();
int newValue = oldValue + 1;
if (compareAndSet(oldValue, newValue)) {
return newValue;
}
}

如果更新失败,说明期间有其他线程改过这个值,那就重新读取再试。

CAS 和 synchronized 的区别#

两者都可以解决并发更新问题,但方式不同。

对比synchronizedCAS
思路加锁排队不加锁,失败重试
阻塞可能阻塞线程通常不阻塞
适合场景临界区较复杂单个变量或简单状态更新
失败处理线程等待锁CAS 失败后重试

CAS 更适合轻量、简单、冲突不太高的场景。

如果临界区里要做很多业务逻辑,或者需要同时保护多个变量,直接用 CAS 反而会让代码很难维护。

锁相关选择可以看:Java 锁专题:从 synchronized 到分布式锁

CAS 的三个问题#

CAS 不是万能的,常见问题有三个。

1. 自旋消耗 CPU#

CAS 更新失败后通常会重试。

如果竞争很激烈,线程一直失败、一直重试,就会消耗 CPU。

所以 CAS 适合冲突不太高的场景。高冲突场景下,不一定比锁好。

2. 只能保证单个变量原子更新#

CAS 很适合更新单个变量。

比如:

stock.decrementAndGet();

但如果要同时维护多个字段:

库存减少
销量增加
状态变更
写操作日志

就不能简单依赖一个 CAS 解决全部一致性问题。

这种场景更适合事务、锁、状态机或数据库约束。

3. ABA 问题#

ABA 问题是 CAS 的经典问题。

线程 A 读取到值是 1。

期间线程 B 把值从 1 改成 2,又改回 1。

线程 A 再 CAS 时发现值还是 1,就认为没有变过,于是更新成功。

但实际上这个值已经被别人改过两次。

A 读取:1
B 修改:1 -> 2
B 修改:2 -> 1
A 判断:还是 1,CAS 成功

如果业务只关心最终数值,ABA 可能没问题。
如果业务关心过程变化,ABA 就可能出问题。

解决思路是加版本号,比如 AtomicStampedReference

AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 1);

更新时不仅比较值,还比较版本号。

LongAdder#

高并发计数场景下,AtomicLong 可能竞争很激烈。

LongAdder 的思路是把一个热点值拆散成多个分段,多个线程分别更新不同分段,最后求和。

LongAdder adder = new LongAdder();
adder.increment();
adder.increment();
long count = adder.sum();

适合:

  • 访问量统计;
  • QPS 统计;
  • 监控计数;
  • 只需要最终统计值的场景。

不适合:

  • 要求每次读取都绝对精确;
  • 要用当前值参与强一致业务判断;
  • 库存扣减这类不能超卖的场景。

库存扣减应该优先使用数据库条件更新、乐观锁、Redis 预扣等业务方案。

CAS 和乐观锁的关系#

CAS 和乐观锁思想很像。

CAS 是内存层面的比较并交换:

当前值 == 期望值 ? 更新 : 失败

数据库乐观锁也是类似思想:

UPDATE product
SET stock = stock - 1,
version = version + 1
WHERE id = #{productId}
AND version = #{oldVersion};

只有版本号没变,才说明这条数据没有被别人更新过。

区别在于:

  • CAS 操作的是内存变量;
  • 数据库乐观锁操作的是数据库记录;
  • CAS 失败通常自旋重试;
  • 数据库乐观锁失败要结合业务决定是否重试。

CAS 在并发集合里的作用#

很多 JUC 工具内部都会使用 CAS。

比如 ConcurrentHashMap 在插入元素时,会在某些位置使用 CAS 尝试写入节点;竞争复杂时再配合锁控制。

这也是现代并发工具常见的设计方式:

低冲突:CAS 快速完成
高冲突:退化到锁或其他控制方式

所以不要把 CAS 和锁看成完全对立。

很多高质量并发工具,都是 CAS 和锁配合使用。

CAS 适合什么场景#

适合:

  • 单个变量计数;
  • 状态从 A 改到 B;
  • 冲突不高的轻量更新;
  • 并发工具内部实现;
  • 统计类数据累加。

不适合:

  • 多字段强一致更新;
  • 高冲突热点资源;
  • 复杂业务临界区;
  • 更新失败后不能简单重试;
  • 需要跨 JVM 控制的场景。

比如商品库存扣减,如果只是单机内存变量,CAS 可以做 Demo。但真实电商库存通常在数据库或 Redis 里,需要结合业务一致性设计。

总结#

CAS 的核心就是一句话:

先比较,再交换;值没变就更新,值变了就失败重试。

它的优势是轻量、不阻塞,适合简单变量更新。
它的限制是只能处理比较小的原子操作,高冲突下可能自旋消耗 CPU,也要注意 ABA 问题。

学习 CAS 时,重点不是记住某个 API,而是理解这种乐观并发思想。

后面看到 AtomicIntegerConcurrentHashMap、AQS、乐观锁时,就能明白它们背后都有类似的判断逻辑:

我更新前先确认一下,数据还是不是我刚才看到的样子。
Share

If this article helped you, please share it with others!

Java CAS 专题:从 AtomicInteger 到乐观并发
https://mizuki.mysqil.com/posts/java-cas/
Author
梦幻晨风
Published at
2025-04-02
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents