【Java EE】CAS(Compare And Swap)

CAS

在高并发编程中,保证数据一致性一直是个核心难题。传统的互斥锁(如synchronized)通过阻塞线程来避免竞争,但会带来上下文切换的开销。JUC(java.util.concurrent)大量采用了一种无锁算法------CAS(Compare And Swap),它在保证原子性的同时,极大提升了性能。本文将深入理解CAS的原理、应用以及经典的ABA问题。

什么是CAS?

CAS,全称Compare And Swap,即比较并交换。它是一种乐观锁 的实现,依靠硬件指令保证原子性 。它的核心作用是:在不使用锁(如 synchronized)的情况下,保证读取-比较-写入这三步操作不会被其他线程打断

CAS 操作依托 sun.misc.Unsafe 类下的 compareAndSwapXXX 等方法。代码形态如下(OpenJDK 源码片段):

java 复制代码
public final native boolean compareAndSwapObject(Object o, long offset,
                                                 Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset,
                                               long expected, long x);

这些方法会根据字段在对象中的内存偏移量(offset)找到变量,然后执行CAS操作。由于Unsafe涉及底层内存操作,普通应用代码不能直接获取其实例,但JUC包内的原子类已经帮我们封装好了这些能力。

CAS 核心逻辑⭐

一条完整的 CAS 操作包含三个操作数:

  • 内存位置 V :要读取和修改的变量地址(对应代码中的 address
  • 预期原值 A :我们认为内存位置中应该存的值(对应代码中的 expectedValue
  • 新值 B :希望写入的新值(对应代码中的 swapValue
java 复制代码
boolean CAS(address, expectedValue, swapValue) {
    if (address 当前的值 == expectedValue) {
        address = swapValue;   // 将新值写入内存
        return true;           // 更新成功
    }
    return false;              // 值已被修改,本次不更新
}

参数说明

  • address:内存地址(变量位置)
  • expectedValue:期望看到的值(CPU 寄存器1的值)
  • swapValue:要设置的新值(CPU 寄存器2的值)

执行逻辑:当且仅当 V 中的值与 A 相等时,CAS才会通过原子方式将 V 的值更新为 B,否则不做任何操作。无论是否更新成功,最终都会返回 V 的旧值。

注意 :实际 CPU 硬件实现中,比较和交换是原子操作,不会被中断。

CAS_流程图

CAS的应用

a) 原子类(Atomic包)

Java的原子类(AtomicInteger, AtomicLong,AtomicReference 等)正是基于CAS实现的无锁线程安全工具。以 AtomicIntegerincrementAndGet 为例:

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounterDemo {
    // 使用原子类 AtomicInteger 代替普通的 int,保证线程安全
    // private static int count = 0;  // 如果使用此方式,多线程并发累加会导致结果错误
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        // 线程1:对 count 执行 50000 次自增
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // count.getAndIncrement() 等价于 count++
                // 它会原子地返回当前值并加1,底层基于 CAS 实现
                count.getAndIncrement();

                // 如果是 ++count,可以使用:
                // count.incrementAndGet();

                // 如果需要增加指定数值 n(如 count += n),可以使用:
                // count.addAndGet(n);
            }
        });

        // 线程2:同样对 count 执行 50000 次自增
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });

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

        try {
            // 主线程等待 t1 和 t2 执行结束
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 期望输出 100000,原子类保证了最终结果的正确性
        System.out.println("Count: " + count.get());
    }
}

b) 实现自旋锁

利用 AtomicReference 的 CAS 操作,我们可以模拟一把简单的自旋锁。锁的状态由 AtomicReference<Thread> 持有,null 表示释放,非null 表示被某个线程持有。

java 复制代码
import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<>(null);

    public void lock() {
        Thread current = Thread.currentThread();
        // 自旋直到成功获取锁
        while (!owner.compareAndSet(null, current)) {
            // 可在此处让出CPU,减少忙等开销,如 Thread.yield();
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        // 只有持有锁的线程才能释放
        owner.compareAndSet(current, null);
    }

    // 测试代码
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();
        Runnable task = () -> {
            spinLock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 获得锁");
                Thread.sleep(10); // 模拟业务操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + " 释放锁");
                spinLock.unlock();
            }
        };

        new Thread(task, "线程-1").start();
        new Thread(task, "线程-2").start();
    }
}

CAS成功时,锁被当前线程获取;失败则一直自旋尝试(忙等待)。这种方式避免了线程挂起、唤醒的开销,适合临界区执行时间非常短的场景。

ABA问题

什么是ABA问题?

CAS操作仅比较值是否相等,但无法感知值的变化过程。假设某个变量初始值是A,线程1把它改为B,又线程2(或同线程)把它改回A。此时另一个线程进行CAS检测时,发现值仍然是A,就误认为它没有被修改过,继续执行更新------但事实上变量已经经历过"A→B→A"的变化,这种变化在某些场景下可能引发错误。

解决方案:带版本号的CAS

Java提供了 AtomicStampedReferenceAtomicMarkableReference 来解决ABA问题。

  • AtomicStampedReference:内部维护一个[reference, stamp]对,stamp相当于版本号。每次更新时stamp递增,CAS会比较引用和stamp是否同时匹配。
  • AtomicMarkableReference:类似,mark是布尔型的标记,适用于关注节点是否被逻辑删除等场景。

示例:使用 AtomicStampedReference 解决ABA

java 复制代码
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAProblemSolution {
    public static void main(String[] args) {
        String initialRef = "A";
        int initialStamp = 0;
        AtomicStampedReference<String> stampedRef = 
                new AtomicStampedReference<>(initialRef, initialStamp);

        // 模拟线程1:期望 stamp 为 0,更新为 "C" 并 stamp+1
        Thread t1 = new Thread(() -> {
            int stamp = stampedRef.getStamp();
            String ref = stampedRef.getReference();
            // 模拟 ABA 发生
            sleep(100);
            boolean success = stampedRef.compareAndSet(ref, "C", stamp, stamp + 1);
            System.out.println("线程1更新结果: " + success + ",当前引用: " + stampedRef.getReference());
        });

        // 模拟线程2:执行 A -> B -> A 的ABA操作,同时stamp变化
        Thread t2 = new Thread(() -> {
            // A -> B (stamp 0->1)
            int stamp = stampedRef.getStamp();
            stampedRef.compareAndSet("A", "B", stamp, stamp + 1);
            System.out.println("线程2: A->B , stamp=" + stampedRef.getStamp());

            // B -> A (stamp 1->2)
            stamp = stampedRef.getStamp();
            stampedRef.compareAndSet("B", "A", stamp, stamp + 1);
            System.out.println("线程2: B->A , stamp=" + stampedRef.getStamp());
        });

        t2.start();
        sleep(50); // 保证t2先执行ABA
        t1.start();
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { }
    }
}

输出示例:

text 复制代码
线程2: A->B , stamp=1
线程2: B->A , stamp=2
线程1更新结果: false,当前引用: A

因为线程1在尝试CAS时,会检查当前stamp(此时为2)是否与它之前读取的stamp(0)一致,显然不一致,因此CAS失败,避免了ABA问题。

CAS的优缺点

优点

  • 高并发性能:无锁设计,线程不会阻塞,避免了上下文切换开销,适合读多写少或临界区极短的场景。
  • 天然避免死锁:由于没有锁的持有与等待,不存在死锁风险(但可能存在活锁,即长时间CAS失败重试)。
  • 粒度细:可以针对单个变量进行原子操作。

缺点

  • ABA问题:需要额外机制处理(如版本号)。
  • 自旋开销:在高竞争下,大量线程反复CAS会消耗CPU资源,甚至出现总线风暴。通常配合CPU pause指令或自适应重试策略。
  • 只能保证一个共享变量的原子性 :若需要对多个变量进行复合原子操作,仍需要锁机制(或使用AtomicReference封装一个不可变对象整体更新)。
  • 底层实现依赖硬件的原子指令,JVM需要高效地暴露这些能力(如通过Unsafe)。
相关推荐
OneT1me1 小时前
CVE-2026-31431 的C语言版本
c语言·开发语言·安全威胁分析
白露与泡影1 小时前
Spring Boot 完整流程
java·spring boot·后端
空中海1 小时前
第二章:Maven进阶篇 — 依赖管理与构建生命周期
java·maven
xun-ming2 小时前
AI时代Java程序员自救手册
java·开发语言·人工智能
DavidSoCool2 小时前
GB28181 PTZCmd 完整指令对照表(8 位 16 进制)+ 详细注释 + 使用说明
java·sip·gb28181
张健11564096482 小时前
C++访问控制与友元
java·开发语言·c++
Sam_Deep_Thinking2 小时前
中小团队需要一个资源微服务
java·微服务·架构
Thanks_ks2 小时前
透过 Copy-On-Write 机制:理解并发编程中的性能与一致性权衡
java·多线程·并发编程·底层原理·写时复制·copyonwrite·性能优
2zcode2 小时前
基于MATLAB改进最大熵法的大规模新能源并网概率潮流计算
开发语言·matlab