每天一个知识点——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 操作。

相关推荐
回家路上绕了弯35 分钟前
深度理解 Lock 与 ReentrantLock:Java 并发编程的高级锁机制
java·后端
Captaincc1 小时前
TRAE 首场 Meetup:8月16日,期待与你在北京相聚
前端·后端·trae
肩塔didi2 小时前
用 Pixi 管理 Python 项目:打通Conda 和 PyPI 的边界
后端·python·github
dylan_QAQ2 小时前
【附录】相对于BeanFactory ,ApplicationContext 做了哪些企业化的增强?
后端·spring
唐诗2 小时前
VMware Mac m系列安装 Windws 11,保姆级教程
前端·后端·github
Lx3522 小时前
Hadoop新手必知的10个高效操作技巧
hadoop·后端
写bug写bug3 小时前
搞懂Spring任务执行器和调度器模型
java·后端·spring
二闹3 小时前
TCP三次握手的智慧:为什么不是两次或四次?
后端·tcp/ip
熊猫片沃子3 小时前
Maven在使用过程中的核心知识点总结
java·后端·maven
集成显卡3 小时前
Rust 实战四 | Traui2+Vue3+Rspack 开发桌面应用:通配符掩码计算器
后端·程序员·rust