3
0
0

Java 多线程:CAS、Atomic 原子类与 ABA 问题

2026-11-03
2026-11-03
文章摘要
|

一、CAS 的核心原理

1. 为什么需要 CAS?

在多线程环境下,多个线程同时修改共享变量,可能出现线程安全问题。

例如:

private static volatile int count = 0;

public void increment() {
    count++;
}

即使 count 使用 volatile 修饰,多个线程同时执行 count++,仍然可能发生丢失更新。

原因是:

读取 count
    ↓
计算 count + 1
    ↓
写回 count

这三个步骤不是一个原子操作。

传统解决方案是使用 synchronized:

public synchronized void increment() {
    count++;
}

但对于简单的变量更新,还可以使用 CAS 实现原子操作。

2. 什么是 CAS?

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

CAS 的核心思想:

先比较当前值是否等于预期值,如果相等,就将当前值更新为新值;如果不相等,就不进行更新。

CAS 涉及三个参数:

参数

含义

V(Value)

内存中的当前值

A(Expected)

预期旧值

B(New Value)

希望更新成的新值

执行逻辑:

if (当前值 V == 预期值 A) {

    将 V 更新为 B;

    返回 true;

} else {

    不修改 V;

    返回 false;
}

例如:

当前值:10

预期值:10

新值:11

执行 CAS(10, 11):

  • 当前值等于预期值,更新为 11,返回 true。

  • 当前值不等于预期值,不进行更新,返回 false。

CAS 最重要的特点是:比较和更新是一个原子操作。

在 HotSpot JVM 中,CAS 通常依靠处理器提供的原子指令等机制实现。

3. 两个线程同时执行 CAS 会怎么样?

假设:

count = 10

线程 A 和线程 B 都希望将 count 加一。

线程 A                  线程 B

读取 count = 10         读取 count = 10
     ↓                       ↓
CAS(10, 11)             CAS(10, 11)
     ↓                       ↓
更新成功                更新失败
     ↓                       ↓
count = 11              发现当前值已经是 11
                             ↓
                        重新读取 count = 11
                             ↓
                        CAS(11, 12)
                             ↓
                        更新成功

最终:

count = 12

线程 B 第一次 CAS 失败,是因为当前值已经发生变化,不再等于预期值 10。

因此需要重新读取当前值,再尝试更新。

4. 什么是 CAS 自旋?

CAS 失败后,如果业务要求更新必须成功,可以不断重新读取当前值并尝试 CAS,直到成功。

这种方式称为 CAS 自旋重试。

Java 8 示例:

import java.util.concurrent.atomic.AtomicInteger;

public class CasDemo {

    private static final AtomicInteger COUNT =
            new AtomicInteger(10);

    public static void increment() {

        while (true) {

            // 读取当前值
            int oldValue = COUNT.get();

            // 计算新值
            int newValue = oldValue + 1;

            // 比较并更新
            if (COUNT.compareAndSet(oldValue, newValue)) {
                break;
            }

            // 失败后重新读取并重试
        }
    }
}

其中:

COUNT.compareAndSet(oldValue, newValue);

表示:

  • 当前值等于 oldValue,更新为 newValue,返回 true。

  • 当前值不等于 oldValue,不进行更新,返回 false。

注意:

compareAndSet() 本身只执行一次 CAS,不会自动无限重试。

上面的 while 循环才实现了自旋重试。

生产开发中,简单的递增操作直接使用 incrementAndGet() 即可,不需要自己手写 CAS 循环。

二、Atomic 原子类

1. 什么是 Atomic 原子类?

Java 在 java.util.concurrent.atomic 包中提供了多种原子类。

它们封装了底层原子操作,可以在多线程环境下安全地更新变量。

常见原子类:

原子类

作用

AtomicInteger

原子操作 int

AtomicLong

原子操作 long

AtomicBoolean

原子操作 boolean

AtomicReference

原子操作对象引用

AtomicIntegerArray

原子操作 int 数组元素

AtomicStampedReference

带版本号的原子引用

AtomicMarkableReference

带布尔标记的原子引用

LongAdder

高并发计数

这些类都属于 Java 8 JDK 自带的类,不需要额外引入第三方依赖。

2. AtomicInteger 常用方法

AtomicInteger count = new AtomicInteger(10);

方法

作用

get()

获取当前值

set(20)

设置当前值

incrementAndGet()

先加一,返回新值

getAndIncrement()

返回旧值,再加一

decrementAndGet()

先减一,返回新值

addAndGet(5)

加五,返回新值

compareAndSet(10, 20)

当前值为 10 才更新为 20

getAndSet(20)

设置为 20,返回旧值

特别注意:

AtomicInteger count = new AtomicInteger(10);

System.out.println(count.incrementAndGet()); // 11

System.out.println(count.getAndIncrement()); // 11

System.out.println(count.get());             // 12

两者区别:

  • incrementAndGet():先更新,再返回新值。

  • getAndIncrement():先返回旧值,再更新。

3. AtomicInteger 底层如何实现?

Java 8 中,AtomicInteger 的原子更新主要依靠 CAS 实现。

其内部涉及 Unsafe 提供的原子操作。

简化理解:

public final int incrementAndGet() {

    // 底层通过原子更新实现递增
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

底层核心逻辑可以理解为:

do {

    oldValue = 读取当前值;

} while (!CAS(oldValue, oldValue + 1));

如果 CAS 失败,就重新读取当前值并重试。

注意:

  • Unsafe 是 JDK 内部实现细节。

  • 生产开发不推荐直接使用 sun.misc.Unsafe。

  • 优先使用 AtomicInteger 等 JUC 提供的公开 API。

4. AtomicReference:原子更新对象引用

AtomicReference 与 AtomicInteger 的区别:

  • AtomicInteger 操作 int。

  • AtomicReference 操作对象引用。

例如:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {

    public static void main(String[] args) {

        AtomicReference<String> reference =
                new AtomicReference<>("A");

        boolean result =
                reference.compareAndSet("A", "B");

        System.out.println(result);          // true
        System.out.println(reference.get()); // B
    }
}

需要注意:

AtomicReference.compareAndSet() 比较的是对象引用是否相同,而不是通过 equals() 比较对象内容。

另外,AtomicReference 保证的是对象引用更新的原子性,并不意味着被引用对象内部的字段也自动线程安全。

5. AtomicLong 与 LongAdder 的区别

高并发计数场景中,除了 AtomicLong,还可以使用 LongAdder。

import java.util.concurrent.atomic.LongAdder;

LongAdder counter = new LongAdder();

counter.increment();

counter.add(5);

long result = counter.sum();

两者区别:

对比

AtomicLong

LongAdder

原子递增

支持

支持线程安全累加

高并发计数

可能产生较多 CAS 竞争

通过分散计数降低竞争

获取数值

get()

sum()

并发读取

原子读取

更新期间不是原子快照

典型场景

精确状态更新、序列计数

QPS、请求次数统计

LongAdder 通过分散计数的方式降低多个线程竞争同一个变量的概率,最后通过 sum() 汇总。

例如:

Spring Boot 服务统计接口请求次数,可以考虑 LongAdder。

但是如果需要根据一个精确数值执行条件更新,例如余额扣减、额度判断,就不能简单使用 LongAdder.sum() 代替原子条件更新。

三、ABA 问题与解决方案

1. 什么是 ABA 问题?

CAS 只检查当前值是否与预期值相同。

但是它无法仅凭当前值判断:

这个值在中间有没有被其他线程修改过。

假设共享变量最初为 A。

初始值:A

线程 A 读取:A

线程 B:A → B → A

线程 A 再次检查:仍然是 A

线程 A 会认为值没有发生变化,CAS 仍然能够成功。

但是实际上,中间已经发生了两次修改。

这就是 ABA 问题。

2. ABA 问题示例

import java.util.concurrent.atomic.AtomicReference;

public class AbaDemo {

    public static void main(String[] args) {

        AtomicReference<String> reference =
                new AtomicReference<>("A");

        // 线程 A 读取初始值
        String expected = reference.get();

        // 模拟其他线程修改:A → B → A
        reference.compareAndSet("A", "B");

        reference.compareAndSet("B", "A");

        // 线程 A 再次尝试更新
        boolean result =
                reference.compareAndSet(expected, "C");

        System.out.println(result);          // true
        System.out.println(reference.get()); // C
    }
}

虽然中间发生了 A → B → A,但最后 CAS 仍然成功。

因为它只检查当前引用是否等于预期引用。

注意:

ABA 不代表所有 CAS 操作都会出现业务错误。

例如普通计数器,只关心当前数值时,ABA 未必产生影响。

但是对于无锁链表、对象状态流转、资源所有权变化等场景,中间发生过变化可能具有业务意义,就需要考虑 ABA。

3. 如何解决 ABA 问题?

Java 提供了:

AtomicStampedReference

核心思想:

不仅比较对象引用,还比较版本号。

原来的 CAS:

当前值 == 预期值

增加版本号后:

当前引用 == 预期引用

并且

当前版本号 == 预期版本号

两个条件同时满足才允许更新。

例如:

初始状态:A,版本 1

线程 B 修改:

A(1) → B(2) → A(3)

线程 A 尝试:

预期 A(1),更新为 C(2)

当前状态 A(3)

引用相同,但版本不同。

CAS 失败!

4. AtomicStampedReference 完整示例

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedDemo {

    public static void main(String[] args)
            throws InterruptedException {

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

        CountDownLatch readDone = new CountDownLatch(1);
        CountDownLatch abaDone = new CountDownLatch(1);

        // 线程 A:先读取旧状态
        Thread threadA = new Thread(() -> {

            int[] stampHolder = new int[1];

            // 同时获取引用和版本号
            String oldValue = reference.get(stampHolder);
            int oldStamp = stampHolder[0];

            readDone.countDown();

            try {
                // 等待线程 B 完成 ABA
                abaDone.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }

            boolean success = reference.compareAndSet(
                    oldValue,
                    "C",
                    oldStamp,
                    oldStamp + 1
            );

            System.out.println("线程 A 更新结果:" + success);

        }, "thread-A");

        // 线程 B:执行 A → B → A
        Thread threadB = new Thread(() -> {

            try {
                readDone.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }

            try {
                reference.compareAndSet("A", "B", 1, 2);

                reference.compareAndSet("B", "A", 2, 3);

            } finally {
                abaDone.countDown();
            }

        }, "thread-B");

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();

        System.out.println("最终值:" + reference.getReference());
        System.out.println("最终版本:" + reference.getStamp());
    }
}

正常执行结果:

线程 A 更新结果:false
最终值:A
最终版本:3

线程 A 最初读取:

引用 A,版本 1

线程 B 修改之后:

引用 A,版本 3

虽然引用重新变回 A,但是版本号已经发生变化。

因此,线程 A 的 CAS 更新失败。

注意:

  • get(int[] stampHolder) 可以同时获取引用和对应版本号。

  • AtomicStampedReference 仍然使用引用相等进行比较。

  • 版本号使用 int,理论上存在溢出的可能,不能认为它可以解决所有并发问题。

四、CAS 与 synchronized 的区别及生产实践

1. CAS 与 synchronized 的区别

对比

CAS

synchronized

核心机制

比较并原子更新

互斥锁

是否需要获取 Monitor

不需要

需要

竞争失败

返回失败,可选择重试

等待获取锁

是否可能阻塞

CAS 本身不需要 Monitor 等待

竞争时可能阻塞

适用场景

简单变量原子更新

多步骤临界区

主要问题

ABA、重试成本

锁竞争、死锁等

CAS 常被称为无锁并发技术。

但需要注意:

无锁不代表没有竞争,也不代表一定比 synchronized 快。

高并发情况下,多个线程可能不断 CAS 失败和重试,产生较大的 CPU 开销。

CAS 更适合操作简单、竞争相对可控的场景。

2. 生产场景:接口调用次数统计

Spring Boot 中可以使用 AtomicLong 统计请求次数:

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Service;

@Service
public class RequestCounterService {

    private final AtomicLong requestCount =
            new AtomicLong(0);

    public void increment() {
        requestCount.incrementAndGet();
    }

    public long getCount() {
        return requestCount.get();
    }
}

这种方式可以保证单个 JVM 内多个线程并发递增的正确性。

如果主要用于高并发统计指标,也可以考虑 LongAdder。

3. CAS 能解决数据库并发更新吗?

不能直接解决。

例如:

private final AtomicInteger stock =
        new AtomicInteger(100);

CAS 只能保证当前 JVM 中 stock 的原子更新。

如果通过 Docker 部署三个 Spring Boot 实例:

Spring Boot A:AtomicInteger stock A

Spring Boot B:AtomicInteger stock B

Spring Boot C:AtomicInteger stock C

每个 JVM 都有自己的 AtomicInteger 对象。

它们之间不共享内存,因此不能依靠 Java CAS 保证数据库库存一致性。

对于数据库库存扣减,更推荐使用数据库条件更新:

UPDATE product
SET stock = stock - 1
WHERE id = #{productId}
  AND stock > 0;

对于需要版本控制的业务,也可以使用数据库乐观锁:

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

如果更新影响行数为 0,说明当前条件不满足,可能是库存不足,也可能是版本发生变化,需要根据业务判断是否重试。

数据库 version 乐观锁与 AtomicStampedReference 的思想类似,都是利用版本变化识别并发冲突,但它们工作在不同的技术层面。

五、重点总结

知识点

核心结论

CAS

比较当前值与预期值,相同才原子更新

compareAndSet()

返回本次 CAS 是否成功

CAS 自旋

CAS 失败后重新读取并重试

AtomicInteger

提供 int 的原子更新

AtomicReference

提供对象引用的原子更新

AtomicLong

适合需要精确原子计数的场景

LongAdder

适合高并发统计

ABA 问题

数据发生 A → B → A,CAS 无法仅凭当前值发现中间变化

AtomicStampedReference

通过引用和版本号共同判断更新条件

synchronized

通过互斥保护临界区

CAS 的局限

高竞争重试、ABA、多变量一致性等问题

需要重点记住的七个结论:

  1. CAS 的比较与更新是原子操作。

  2. CAS 失败不代表线程自动阻塞,也不代表自动重试。

  3. AtomicInteger.incrementAndGet() 可以保证原子递增。

  4. AtomicReference.compareAndSet() 比较对象引用是否相同。

  5. ABA 不一定产生错误,取决于业务是否关心中间状态变化。

  6. AtomicStampedReference 通过版本号识别 ABA。

  7. CAS 不能直接替代数据库事务或分布式锁。

六、常见问题与答案

1. CAS 为什么能够保证原子性?

因为比较和更新由底层原子操作共同完成,不会出现检查通过后、写入之前被其他线程插入修改的问题。

2. CAS 是自旋锁吗?

CAS 是一种原子操作,自旋是 CAS 失败后循环重试的行为。两者相关,但不是同一个概念。

3. CAS 一定比 synchronized 快吗?

不一定。低竞争、操作简单时 CAS 通常有优势,但高竞争下不断重试可能消耗大量 CPU。

4. AtomicInteger 和 volatile int 有什么区别?

volatile int 只提供单次读写的可见性及相关内存语义,不保证 count++ 的原子性;AtomicInteger 提供 incrementAndGet() 等原子复合操作。

5. AtomicReference 可以保证对象内部线程安全吗?

不能。它只保证引用的原子更新,不自动保证被引用对象的内部字段安全。

6. 什么是 ABA 问题?

一个值从 A 改成 B,再改回 A。CAS 只比较当前值与预期值,无法判断中间是否发生过变化。

7. AtomicStampedReference 如何解决 ABA?

通过同时比较引用和版本号,检测引用恢复原值但版本已经变化的情况。

8. 什么情况下推荐使用 LongAdder?

大量线程并发更新统计指标,且不要求 sum() 在并发更新期间提供原子快照时。

9. CAS 可以保证多个 Spring Boot 实例的库存安全吗?

不能。不同 JVM 的 CAS 对象相互独立,数据库库存应通过数据库条件更新、事务等机制保证并发安全。

七、复习测试

  1. CAS 的三个参数分别是什么?CAS 如何判断是否更新成功?

  2. 为什么两个线程同时执行 CAS,不会出现普通 count++ 那样的丢失更新?

  3. 什么是 CAS 自旋?compareAndSet() 本身会不会自动重试?

  4. AtomicInteger 和 volatile int 有什么区别?

  5. AtomicReference.compareAndSet() 使用 equals() 还是引用相等进行比较?

  6. LongAdder 为什么适合高并发统计?它的 sum() 有什么限制?

  7. 什么是 ABA 问题?为什么 A → B → A 后 CAS 仍然可能成功?

  8. AtomicStampedReference 如何通过版本号解决 ABA?

  9. CAS 和 synchronized 分别适合什么场景?

  10. 为什么多个 Spring Boot 实例不能依靠 AtomicInteger 保证数据库库存安全?

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论