每天一个知识点——Java之CAS操作

参考:

破解Java CAS操作:解锁无锁编程的奥秘与陷阱 - 云原生实践

Java 魔法类 Unsafe 详解 | JavaGuide

CAS 详解 | JavaGuide

一. CAS基本概念

CAS(Compare-And-Swap)是一种无锁编程的核心技术,用于实现多线程环境下的原子操作。其核心思想是"先比较,再交换",具体操作包含三个参数:

  • 内存位置(变量V)
  • 预期原值(A)
  • 新值(B)

当且仅当内存位置V的值等于预期值A时,才会将V的值更新为B,否则不执行操作。整个过程是原子的,无需加锁即可保证线程安全。

二. Unsafe

在 Java 中,实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是Unsafe

为什么需要Unsafe?

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再"安全",因此对 Unsafe 的使用一定要慎重。

Unsafe类中的 CAS 方法是native方法。native关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的硬件指令来实现原子操作。也就是说,Java 语言并没有直接用 Java 实现 CAS。

三. Java中实现CAS操作

3.1 Java实现

Java中的Unsafe类提供了底层操作内存、线程通信等功能,它是实现CAS机制的基础。Unsafe类中的compareAndSwapObject,compareAndSwapInt,compareAndSwapLong方法可以用来实现CAS操作:

java 复制代码
/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

这个方法需要传入对象、偏移量、期望值和新值,如果对象的偏移量处的值等于期望值,则将其更新为新值,并返回true;否则,不做任何操作,并返回false

CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数------内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg

3.2 典型应用

在 JUC 包的并发工具类中大量地使用了 CAS 操作。在 Unsafe 类中,提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作。

csharp 复制代码
private volatile int a = 0; // 共享变量,初始值为 0
private static final Unsafe unsafe;
private static final long fieldOffset;

static {
    try {
        // 获取 Unsafe 实例
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        unsafe = (Unsafe) theUnsafe.get(null);
        // 获取 a 字段的内存偏移量
        fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
    } catch (Exception e) {
        throw new RuntimeException("Failed to initialize Unsafe or field offset", e);
    }
}

public static void main(String[] args) {
    CasTest casTest = new CasTest();

    Thread t1 = new Thread(() -> {
        for (int i = 1; i <= 4; i++) {
            casTest.incrementAndPrint(i);
        }
    });

    Thread t2 = new Thread(() -> {
        for (int i = 5; i <= 9; i++) {
            casTest.incrementAndPrint(i);
        }
    });

    t1.start();
    t2.start();

    // 等待线程结束,以便观察完整输出 (可选,用于演示)
    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

// 将递增和打印操作封装在一个原子性更强的方法内
private void incrementAndPrint(int targetValue) {
    while (true) {
        int currentValue = a; // 读取当前 a 的值
        // 只有当 a 的当前值等于目标值的前一个值时,才尝试更新
        if (currentValue == targetValue - 1) {
            if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
                // CAS 成功,说明成功将 a 更新为 targetValue
                System.out.print(targetValue + " ");
                break; // 成功更新并打印后退出循环
            }
            // 如果 CAS 失败,意味着在读取 currentValue 和执行 CAS 之间,a 的值被其他线程修改了,
            // 此时 currentValue 已经不是 a 的最新值,需要重新读取并重试。
        }
        // 如果 currentValue != targetValue - 1,说明还没轮到当前线程更新,
        // 或者已经被其他线程更新超过了,让出CPU给其他线程机会。
        // 对于严格顺序递增的场景,如果 current > targetValue - 1,可能意味着逻辑错误或死循环,
        // 但在此示例中,我们期望线程能按顺序执行。
        Thread.yield(); // 提示CPU调度器可以切换线程,减少无效自旋
    }
}

需要注意的是:

  1. 自旋逻辑: compareAndSwapInt 方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如 while(true) 循环),不断尝试直到 CAS 操作成功。
  2. AtomicInteger 的实现: JDK 中的 java.util.concurrent.atomic.AtomicInteger 类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的 getAndIncrement(), compareAndSet() 等方法。直接使用 AtomicInteger 通常是更安全、更推荐的做法,因为它封装了底层的复杂性。
  3. ABA 问题: CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用 AtomicStampedReference 来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。
  4. CPU 消耗: 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如 Thread.sleep()LockSupport.parkNanos())来优化。

3.3 CAS的存在问题

1.ABA问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。

2. 循环时间开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

如果 JVM 能够支持处理器提供的pause指令,那么自旋操作的效率将有所提升。pause指令有两个重要作用:

  1. 延迟流水线执行指令:pause指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。
  2. 避免内存顺序冲突:在退出循环时,pause指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。

3. 只能保证一个共享变量的原子操作

CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了AtomicReference类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用AtomicReference来执行 CAS 操作。

相关推荐
I'm Jie3 分钟前
告别重复编码!SpringBoot 字段变更(新旧值)日志工具类的规范化设计与优雅实现
java·spring boot·后端
开心猴爷19 分钟前
Bundle Id 创建与管理的工程化方法,一次团队多项目协作中的流程重构
后端
databook20 分钟前
用样本猜总体的秘密武器,4大抽样分布总结
后端·python·数据分析
小坏讲微服务1 小时前
SpringBoot4.0整合Scala完整使用
java·开发语言·spring boot·后端·scala·mybatis
泉城老铁1 小时前
windows服务器mysql数据库备份脚本
java·后端·mysql
却尘1 小时前
用 25 个概念彻底看懂SQL多维分析的底层逻辑
后端·sql·mysql
白衣鸽子1 小时前
JsonUtils:打造企业级的序列化与反序列化瑞士军刀
后端·开源
计算机学姐1 小时前
基于Python的校园美食推荐系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·推荐算法
q***48411 小时前
十八,Spring Boot 整合 MyBatis-Plus 的详细配置
spring boot·后端·mybatis