AQS 的 `state`:volatile + CAS 如何撑起原子性与可见性

AQS 的核心"可扩展点"看起来很多,但底层抓住一个东西就够了:

  • AQS 用一个 volatile int state 表示同步状态 ,所有的"能不能进临界区"最终都落在对 state 的 CAS 更新上。

你必须记住的 3 句话(面试直出):

  • volatile state 让线程看到"最新同步状态",避免读到旧值。
  • CAS 让"从旧状态到新状态"的竞争具备原子性,决定谁能成功。
  • AQS 在 CAS 失败后用队列 + park/unpark 控制成本,避免所有线程无序自旋。

1. state 不是"锁状态",是"同步器状态"

state 的含义由同步器自定义:

  • ReentrantLockstate 常用来表示重入次数(0=未锁,>0=已锁且次数)
  • Semaphorestate 表示许可数量
  • CountDownLatchstate 表示计数器(到 0 才放行)

面试表达:

  • AQS 不关心业务语义,它只提供 state 的原子修改与排队阻塞模板

2. 为什么 state 必须是 volatile

volatile 解决两件事:

  • 可见性:一个线程 release 后,其他线程能看到 state 的变化
  • 有序性(happens-before):确保 release 前的写,在 acquire 后可见(配合具体实现与内存语义)

面试时更"工程化"的说法:

  • 线程 A 在临界区里写的数据,必须在它 unlock 之后对线程 B 可见;否则就算"拿到了锁",读到的也可能是旧数据。

你可以把它理解成:

  • 没有 volatile,线程可能一直读到旧值,出现"锁释放了但别人看不见"。

3. 为什么仅有 volatile 不够,还要 CAS

volatile 只能保证读写可见、有序,但不保证复合操作原子性。

例如独占锁获取时:

  • 你需要把 state 从 0 改成 1
  • 如果两个线程同时看到 0,再同时写 1,必须只有一个成功

所以必须使用 CAS:

  • compareAndSetState(expect, update)

其本质:

  • CPU 原子指令支持的"比较并交换",成功者获得资格

补充一个常见追问:CAS 失败了会怎样?

  • 失败说明有人更早更新了 state(你看到的期望值已经过期)
  • AQS 的做法不是"永远死循环 CAS",而是:
    • 先走快路径抢一次
    • 失败再入队
    • 入队后在合适时机再竞争,仍失败就 park

4. 一个最小可运行的"自定义独占锁"骨架

下面是 AQS 的典型使用姿势(用于理解,不追求功能完整):

java 复制代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class SimpleMutex implements Lock {

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            // 只允许从 0 -> 1
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1 && getExclusiveOwnerThread() == Thread.currentThread();
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    @Override public void lock() { sync.acquire(1); }
    @Override public boolean tryLock() { return sync.tryAcquire(1); }
    @Override public void unlock() { sync.release(1); }
    @Override public Condition newCondition() { return sync.newCondition(); }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }
}

关键点:

  • 原子性来自 CAScompareAndSetState(0, 1)
  • 排队/阻塞来自 AQSacquire(1) 内部处理入队、park
  • 释放唤醒来自 AQSrelease(1) 会唤醒后继节点

你在面试里可以补一句"owner 的意义":

  • setExclusiveOwnerThread 不是为了同步(同步靠 state + 队列),而是为了提供"当前谁持有独占资源"的语义支持(例如可重入、校验是否非法释放)。

5. 常见误区

  • 误区 1:volatile 就是原子

    • 错。volatile 不能保证 state++ 这种复合操作原子。
  • 误区 2:CAS 一定比 synchronized 快

    • 错。竞争激烈/临界区长时,自旋重试会让 CPU 飙升。
  • 误区 3:state 只能 0/1

    • 错。ReentrantLock 用 state 表示重入次数;Semaphore 用 state 表示许可。
  • 误区 4:CAS 失败就一直自旋直到成功

    • 错。AQS 的核心价值之一就是:让失败者进队列、阻塞等待,避免所有线程一起空转。

6. 口述检查点(你要能讲清楚)

  • Q:state 为什么是 int,不是 long?

    • A:多数同步器用 int 足够;更重要是 AQS 设计目标是轻量、通用,扩展语义由上层决定。
  • Q:compareAndSetState 是怎么实现的?

    • A:底层是 Unsafe/VarHandle 调用 CPU CAS 指令,保证原子更新。
  • Q:为什么 acquire 要先 CAS,失败才入队?

    • A:减少不必要的阻塞;多数情况下锁可能很快释放,先快路径抢一次更高效。
  • Q:state 的修改和临界区内的读写之间,怎么建立可见性关系?

    • A:unlock 过程中对 state 的写与唤醒语义,配合 volatile 的内存语义,使得后续成功 acquire 的线程能看到之前临界区内的写入结果(在 JMM 下形成同步关系)。

7. 30 秒背诵稿

AQS 用一个 volatile int state 表示同步状态,volatile 保证可见性与一定的有序性;但"从旧状态更新到新状态"的竞争必须靠 CAS 才能保证原子性,所以获取锁本质是 CAS 改 state,成功就进入临界区,失败就进入 AQS 队列并 park 阻塞;释放时把 state 复位并唤醒队列后继。

相关推荐
2301_788770552 小时前
OJ模拟5
数据结构·算法
羊小猪~~2 小时前
算法/力扣--字符串经典题目
c++·考研·算法·leetcode·职场和发展·哈希算法
zxfBdd2 小时前
idea + spark 报错:object hy is not a member of package com.cmcc
java·ide·intellij-idea
Zik----2 小时前
Windows安装cuda
前端·ui·xhtml
攒了一袋星辰2 小时前
10万级用户数据日更与定向推送系统的可靠性设计
java·数据库·算法
王杨游戏养站系统2 小时前
3分钟搭建1个游戏下载站网站教程!SEO站长养站系统!
开发语言·前端·游戏·游戏下载站养站系统·游戏养站系统
是上好佳佳佳呀2 小时前
【前端(三)】CSS 属性梳理:从字体文本到背景表格
前端·css
凸头2 小时前
从“搜了就答”到“智能决策”:拥抱 RAG 2.0 时代的架构演进 ——Java 后端工程师视角下的 AI 应用工程化落地
java·人工智能·架构·rag
nap-joker2 小时前
PIPE4:快速PPI预测器,用于综合的跨物种和跨物种相互作用组
算法·多模态生物医学数据分析·蛋白质互作网络