CAS与AQS

维度CASAQS
定位 CPU原子指令 / 底层并发原语 Java实现的、构建同步器的高级框架
核心思想 乐观锁,无锁并发,比较并交换 模板方法模式,队列 + 状态管理
实现级别 硬件指令(汇编级别) Java类库级别(基于CAS和队列)
关注点 保证单个共享变量的原子性更新 管理多线程对共享资源的访问秩序
解决问题 低粒度、简单的原子操作(如i++) 高粒度、复杂的同步控制(如锁、信号量)
性能特点 竞争不激烈时性能极高;激烈时自旋消耗CPU 竞争激烈时,会将线程排队挂起,避免CPU空转
关系 是AQS实现的基石,AQS用CAS操作state和队列节点 是CAS的高级使用者,将CAS、队列、线程调度结合

 

image

AQS(AbstractQueuedSynchronizer)

1. 是什么?

AQS是JUC(java.util.concurrent)包中一个用于构建锁和同步器的核心框架。它是一个抽象类,使用了模板方法设计模式。

2. 核心原理

AQS内部维护了一个双向FIFO队列(CLH队列的变种)来管理等待获取资源的线程,同时使用一个volatile的int类型变量state来表示同步状态。

  • state:不同的锁有不同的含义。

    • 在ReentrantLock中,state表示持有锁的线程的重入次数。

    • 在Semaphore中,state表示可用的许可数量。

    • 在CountDownLatch中,state表示计数器数值。

  • 同步队列:当线程获取资源失败时,AQS会将当前线程封装成一个Node节点,CAS地插入队尾,然后挂起线程。当资源释放时,会唤醒队列中的线程再次尝试获取。

3. 在Java中的体现与应用

  • ReentrantLock(可重入锁)

  • Semaphore(信号量)

  • CountDownLatch(倒计时闩)

  • ReentrantReadWriteLock(读写锁)

  • ThreadPoolExecutor.Worker(线程池工作线程)

这些类内部都持有一个AQS的子类(作为内部类),并通过重写AQS的tryAcquire、tryRelease等方法,定义了各自的同步逻辑。

4. 如何工作(以ReentrantLock为例)

  1. 线程调用lock()方法。

  2. AQS子类尝试用CAS操作去修改state(从0改为1)。如果成功,则获取锁,并记录当前线程。

  3. 如果失败(state不为0,可能是锁被占用),则将线程加入等待队列并挂起。

  4. 持有锁的线程调用unlock()时,会释放锁(修改state),并CAS地唤醒队列中的下一个线程。

可以看到,AQS内部的状态管理、入队出队操作,都大量依赖CAS来保证线程安全。

CAS(Compare-And-Swap)

1. 是什么?

CAS是一条CPU原子指令(在Java中通过sun.misc.Unsafe类或java.util.concurrent.atomic包下的原子类暴露给开发者使用)。它的操作是不可分割的。

2. 核心原理

它包含三个操作数:内存位置(V)、预期原值(A)、新值(B)。

  1. 检查内存位置V的值是否等于预期值A。

  2. 如果相等,则处理器会自动将该位置值更新为新值B。

  3. 如果不相等,说明有其他线程修改了V,操作失败(通常选择重试或放弃)。

伪代码表示:

boolean compareAndSwap(int memoryAddress, int expectedValue, int newValue) {
    if (memoryAddress.value == expectedValue) {
        memoryAddress.value = newValue;
        return true;
    }
    return false;
}

3. 在Java中的体现与应用

  • AtomicInteger、AtomicLong、AtomicReference等:这些原子类的incrementAndGet()、compareAndSet()等方法底层都使用CAS。

  • 示例:

    AtomicInteger count = new AtomicInteger(0);
    // 以下操作是线程安全的,底层使用CAS
    count.incrementAndGet(); // 相当于 count++
    boolean success = count.compareAndSet(1, 2); // 如果当前值是1,就改成2

4. 优点与缺点

  • 优点:无锁,避免了线程挂起和调度的开销,在高并发竞争不激烈的场景下性能极高。

  • 缺点:

    • ABA问题:一个值从A变成B,又变回A,CAS检查时会认为它没变。解决方案是使用带版本号的AtomicStampedReference。

    • 循环时间长开销大:如果竞争激烈,CAS长时间失败会不停重试(自旋),消耗CPU。

    • 只能保证一个共享变量的原子操作。

举例说明:假设初始 value = 5。

  1. 线程A执行 incrementAndGet():

    • 读取 oldValue = 5。

    • 计算 newValue = 6。

    • 在它即将执行CAS的瞬间,线程B抢先完成了整个incrementAndGet(),把value从5改成了6。

  2. 现在线程A执行CAS:compareAndSwap(this.value, 5, 6)

    • 检查:当前内存中的this.value已经是6了,而预期值oldValue是5。

    • 结果:6 != 5,CAS失败!返回false。

  3. 线程A进入下一轮循环:

    • 重新读取 oldValue = 6(最新的值)。

    • 重新计算 newValue = 7。

    • 再次尝试CAS:compareAndSwap(this.value, 6, 7)。这次如果没有其他线程干扰,就会成功。

更直观的AtomicInteger源码片段

看AtomicInteger.getAndIncrement()的简化版实现(OpenJDK源码思路):

public final int getAndIncrement() {
    for (;;) { // 自旋循环
        int current = get(); // 【读取】当前值,作为预期值
        int next = current + 1; // 【计算】新值
        if (compareAndSet(current, next)) { // 【CAS尝试写入】
            return current; // 成功则返回旧值
        }
        // 失败则循环重试
    }
}
public final boolean compareAndSet(int expect, int update) {
    // 调用Unsafe类的native方法,底层就是CPU的CAS指令
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

如何避免CAS无限自旋?

  1. 指数退避(Exponential Backoff)

    java
    int retries = 0;
    while (!cas()) {
        retries++;
        if (retries > MAX_RETRIES) {
            // 回退到阻塞锁或其他策略
            lock.lock();
            try {
                // 执行操作
            } finally {
                lock.unlock();
            }
            break;
        }
        // 等待时间逐渐增长
        Thread.sleep(Math.min(100, 1 << retries));
    }
  2. 引入随机性

    java
    while (!cas()) {
        // 随机等待,打破同步
        Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100));
    }
  3. 使用混合策略

    • 先自旋尝试几次

    • 失败后让出CPU(Thread.yield())

    • 再失败就进入等待队列(如AQS做的那样)

  4. 限制自旋次数

    java
    for (int i = 0; i < MAX_SPINS; i++) {
        if (cas()) {
            return success;
        }
    }
    // 自旋超过阈值,转用阻塞方式

Java中的实际处理

Java的AtomicInteger等类没有无限自旋的保护,它们依赖于:

  • JVM和操作系统的线程调度公平性

  • 实际应用中竞争不会如此极端

  • 如果真遇到极端竞争,说明应该换用阻塞同步(如synchronized或ReentrantLock)

总结:
是的,CAS理论上可能导致线程无限自旋,尤其是:

  1. 竞争非常激烈,某些线程总是抢不到"窗口期"

  2. 线程逻辑对称,导致活锁

  3. 没有退避机制

在实际编程中,高竞争场景下使用CAS需要非常小心,通常要配合:

  • 退避策略

  • 竞争检测

  • 必要时回退到阻塞同步

这也是为什么Java的AQS框架并不是纯自旋,而是将线程放入队列并挂起——它避免了无限自旋,用阻塞换取了公平性和稳定性。

方法是否有自旋作用返回值
compareAndSet(expect, update) ❌ 没有 单次CAS尝试 boolean (成功/失败)
getAndIncrement() ✅ 有 原子递增(必须成功) int (旧值)
incrementAndGet() ✅ 有 原子递增(必须成功) int (新值)
getAndAdd(delta) ✅ 有 原子加delta(必须成功) int (旧值)
weakCompareAndSet() ❌ 没有 单次弱内存语义CAS尝试 boolean
 
posted @ 2026-01-07 14:50  a快乐码农  阅读(91)  评论(0)    收藏  举报