《并发编程的“万能钥匙”:一文读懂 AQS 设计哲学》

11 阅读10分钟

引言

提到 Java 并发编程,我们绕不开一个幕后英雄——AbstractQueuedSynchronizer(简称 AQS)。它是实现各类同步器(如 ReentrantLock、CountDownLatch)的“地基”。

很多开发者可能会觉得:“我平时只用锁,又不写锁,为什么要学 AQS?”其实,理解 AQS 是从“会用”进阶到“精通”的关键。你是否好奇过:为什么 ReentrantLock 不需要像 synchronized 那样锁定一个具体的对象,却依然能指挥线程排队、阻塞和唤醒?读懂了 AQS,你就能看透这些工具底层的魔法。

AQS 的核心设计理念

我们先来看 AQS的类图 在这里插入图片描述 (此图摘自《并发编程之美》, 推荐想要深入了解JUC的读者可以去阅读, 同样的还有《并发编程的艺术》这本书)

由此图可以了解到 AQS是一个FIFO(先进先出)的双向队列 内部通过维护 Head 和 Tail 来实现元素的入队与出队

在AQS中还维护了一个状态信息 state, 可以通过其内部提供的方法改变这个值 这个可以由你自己来定义 比如说JDK常见并发工具中 AQS 使用一个 volatile int 类型的成员变量 state 来表示同步状态。有趣的是,在不同的 AQS 实现类中,state 被赋予了完全不同的语义:

  • ReentrantLock: state 表示锁的重入次数。当 state 为 0 时表示锁空闲;当 state > 0 时,表示当前线程已占用锁,数值即为重入的次数。

  • ReentrantReadWriteLock: state 利用位运算被拆分为两部分。高 16 位表示读锁(共享锁)的获取次数,低 16 位表示写锁(独占锁)的可重入次数。

  • Semaphore: state 表示当前可用的许可(Permits)数量。

  • CountDownLatch: state 表示倒计时计数器的当前值。

AQS 内部提供了一个核心内部类 ConditionObject,它是 Java 代码层面对管程模式中“等待/通知”机制的实现。

本质上,AQS 的运作依赖于两类队列的协同工作:

同步队列 : 维护没抢到锁的线程,负责锁的争抢与阻塞。

条件队列 : 由 ConditionObject 维护,存储**“抢到了锁但调用 await() 主动挂起”**的线程。

这就是 ReentrantLock 等工具无需依赖 JVM 底层对象监视器(Monitor)锁的奥秘——它通过 Java 代码显式地管理这两类队列,实现了比 synchronized 更灵活的线程调度与唤醒机制。

下面给出具体例子帮你理解AQS的条件队列

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * AQS 多 Condition 演示:自定义阻塞队列
 * 核心优势:一把锁,两个队列("不满"队列 和 "不空"队列),实现精准唤醒
 */
public class BoundedBuffer<T> {
    private final Lock lock = new ReentrantLock();
    // Condition 1: 也就是"不满"队列,生产者在这里排队,等待队列不满
    private final Condition notFull = lock.newCondition();
    // Condition 2: 也就是"不空"队列,消费者在这里排队,等待队列不空
    private final Condition notEmpty = lock.newCondition();

    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    // 生产者:放入数据
    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            // 1. 如果队列满了,生产者去 notFull 队列等待
            while (queue.size() == capacity) {
                System.out.println("【生产者】队列满了,我去 notFull 房间排队睡觉...");
                notFull.await(); 
            }

            queue.add(item);
            System.out.println("【生产者】生产了一个: " + item);

            // 2. 核心点:只唤醒"notEmpty"队列里的消费者
            // 告诉消费者:现在有东西吃了,快醒醒!
            // 即使还有其他生产者在睡觉,也不会吵醒他们(避免了无意义的竞争)
            notEmpty.signal(); 
            
        } finally {
            lock.unlock();
        }
    }

    // 消费者:取出数据
    public T take() throws InterruptedException {
        lock.lock();
        try {
            // 1. 如果队列空了,消费者去 notEmpty 队列等待
            while (queue.size() == 0) {
                System.out.println("【消费者】队列空了,我去 notEmpty 房间排队睡觉...");
                notEmpty.await();
            }

            T item = queue.poll();
            System.out.println("【消费者】消费了一个: " + item);

            // 2. 核心点:只唤醒"notFull"队列里的生产者
            // 告诉生产者:我有空位了,你可以继续生产了!
            notFull.signal();
            
            return item;
        } finally {
            lock.unlock();
        }
    }

    // 测试主函数
    public static void main(String[] args) {
        BoundedBuffer<Integer> buffer = new BoundedBuffer<>(2); // 容量只有2

        // 启动生产者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.put(i);
                    Thread.sleep(100); // 模拟耗时
                }
            } catch (InterruptedException e) { e.printStackTrace(); }
        }, "Producer").start();

        // 启动消费者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.take();
                    Thread.sleep(1000); // 消费者消费得很慢,容易导致队列满
                }
            } catch (InterruptedException e) { e.printStackTrace(); }
        }, "Consumer").start();
    }
}

这种实现的好处是 只用了一把锁就可以实现条件唤醒

这种设计在JAVA的并发容器也很常见 比如说线程池(因为最近一直死磕线程池就拿它的阻塞队列来举例了 )的 ArrayBlockingQueue

它其实就用了一把锁的两个条件队列实现了 队头和队尾插入互不干扰

 public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /*
     * Much of the implementation mechanics, especially the unusual
     * nested loops, are shared and co-maintained with ArrayDeque.
     */

    /**
     * Serialization ID. This class relies on default serialization
     * even for the items array, which is default-serialized, even if
     * it is empty. Otherwise it could not be declared final, which is
     * necessary here.
     */
    private static final long serialVersionUID = -817911632652898426L;

    /** The queued items */
    @SuppressWarnings("serial") // Conditionally serializable
    final Object[] items;

    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

    /** Number of elements in the queue */
    int count;

    /*
     * Concurrency control uses the classic two-condition algorithm
     * found in any textbook.
     */

    /** Main lock guarding all access */
    final ReentrantLock lock;

    /** Condition for waiting takes */
    @SuppressWarnings("serial")  // Classes implementing Condition may be serializable.
    private final Condition notEmpty;

    /** Condition for waiting puts */
    @SuppressWarnings("serial")  // Classes implementing Condition may be serializable.
    private final Condition notFull;

你可以看到它的队列分为notEmpty 和 notFull 分别对应队列满的时候插入 和 队列空的时候消费

这种设计的精髓在于: 我们仅用 一把锁 就实现了 多路精准唤醒,这是原生 synchronized 配合 Object.wait() 无法做到的(后者只能随机唤醒或全员唤醒)。

这种模式在 JUC 并发容器中非常普遍。以我们常用的阻塞队列 ArrayBlockingQueue 为例:它内部维护了一把独占锁和两个条件队列——notEmpty 和 notFull。

notFull:用于管理队列已满时被阻塞的“生产者”;

notEmpty:用于管理队列为空时被阻塞的“消费者”。 通过这种分离,生产者在生产数据后,只需定向唤醒 notEmpty 中的消费者,而不会错误地唤醒其他生产者。这种“各司其职”的等待机制,极大地减少了无效竞争和上下文切换。

那么 我们应该如何 去修改条件变量state 来实现我们想要的操作呢

AQS 的两种资源共享模式

AQS 的核心原理在于对 同步状态 (state) 的原子性管理。根据资源被线程占用的方式不同,AQS 定义了两种主要的操作模式:独占模式 (Exclusive)共享模式 (Shared)

1. 核心方法体系 AQS 针对这两种模式提供了对应的模板方法:

  • 独占模式: acquire(int arg)release(int arg)
  • 共享模式: acquireShared(int arg)releaseShared(int arg) (注:两者均有支持响应中断的 Interruptibly 版本)

2. 独占模式 解析 在独占模式下,资源是与具体线程绑定的,同一时刻只能被一个线程持有。

  • 代表组件: ReentrantLock

  • 工作流程:

    • 当线程 A 获取锁时,AQS 通过 CASstate 从 0 修改为 1,并标记当前线程为锁的持有者 (exclusiveOwnerThread)。
    • 可重入性: 如果线程 A 再次获取锁,发现自己已经是持有者,仅需将 state 累加(从 1 变为 2),表示重入次数。
    • 阻塞机制: 此时若线程 B 尝试获取锁,发现 state 不为 0 且持有者不是自己,获取失败,随即被封装成 Node 节点加入 AQS 阻塞队列挂起。

3. 共享模式 解析 在共享模式下,资源与具体线程不强相关,允许多个线程同时获取。

  • 代表组件: Semaphore (信号量)、CountDownLatch

  • 工作流程:

    • 多个线程通过 CAS 竞争共享资源。
    • Semaphore 为例:当线程调用 acquire() 时,会检查当前 state(剩余许可数)是否满足需求。
    • 成功: 如果资源充足(state > 0),则通过 CAS 扣减 state,线程继续执行。
    • 失败: 如果资源不足,线程才会进入 AQS 阻塞队列等待唤醒。

最后实践是记忆的最好导师 大家可以自己写一个简易锁来加深对AQS的理解

实战:自己手写一个简易锁 (Implementation)

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 一个极简的不可重入独占锁
 * 核心:只关注 state 0->1 和 1->0 的过程,其余排队阻塞逻辑全交给 AQS
 */
public class NonReentrantLock implements Lock {

    // 1. 定义一个 Sync 继承 AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        
        // 尝试获取锁
        @Override
        protected boolean tryAcquire(int arg) {
            // CAS 尝试将 state 从 0 修改为 1
            // 如果成功,说明抢锁成功
            if (compareAndSetState(0, 1)) {
                // 标记当前线程为锁的持有者(用于可重入检查或监控,这里仅做标记)
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            // 失败返回 false,AQS 会自动将当前线程放入阻塞队列
            return false;
        }

        // 尝试释放锁
        @Override
        protected boolean tryRelease(int arg) {
            // 如果当前 state 为 0,说明根本没锁,抛异常
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // 清空持有者
            setExclusiveOwnerThread(null);
            // 修改 state 为 0 (因为是独占模式,且只有持有锁的线程才能释放,所以不需要 CAS)
            setState(0);
            return true;
        }

        // 判断锁是否被占用
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        
        // 提供 Condition
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    // 2. 将操作代理给 Sync
    private final Sync sync = new Sync();

    @Override
    public void lock() {
        // 调用 AQS 的模板方法 acquire
        // AQS 会回调我们写的 tryAcquire,失败则入队阻塞
        sync.acquire(1);
    }

    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    @Override
    public void unlock() {
        // 调用 AQS 的模板方法 release
        // AQS 会回调 tryRelease,并自动唤醒队列中的下一个线程
        sync.release(1);
    }

    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }

    // 省略其他非核心接口实现...
    public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); }
}
public class LockDemo {
    private static int count = 0;
    private static NonReentrantLock lock = new NonReentrantLock();

    public static void add() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 启动 10 个线程,每个加 1000 次
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) add();
            });
            threads[i].start();
        }

        for (Thread t : threads) t.join();

        // 预期结果:10000
        System.out.println("Final Count: " + count);
    }
}

注意我们实现的锁是不可重入的 : 如果在同一个线程中连续调用两次 lock():

lock.lock(); // 第一次:state 0 -> 1,成功 lock.lock(); // 第二次:state 已经是 1,tryAcquire CAS 失败 -> 返回 false // 结果:当前线程被 AQS 放入队列并挂起,导致自己等待自己释放锁 -> 死锁!

如果要实现重入还得实现一些逻辑 比如判断当前线程是不是已经获取锁了 然后把状态用CAS的操作 加一 这里的CAS调用的是UNSAFE的 本地方法 大概意思就是比较并且交换 也就是我们常说的乐观锁

在这里插入图片描述 **

总结 (Conclusion)

** 回顾全文,我们从 AQS 的**“状态 (State) + 队列 (Queue)”** 这一核心模型出发,深入剖析了它如何通过 CLH 变体队列管理线程的排队与唤醒,又如何利用 ConditionObject 实现精准的“等待/通知”机制。

AQS 之所以被称为 Java 并发包的“基石”,是因为它极好地体现了 “分离变与不变” 的设计哲学:

不变的是机制: 线程的阻塞、唤醒、入队、出队,这些复杂的底层操作由 AQS 统一封装,保证了稳定性和高性能。

变化的是策略: 什么是“获取锁”?什么是“资源不够”?这些具体的业务逻辑通过 模板方法模式 (Template Method Pattern) 留给子类(如 ReentrantLock、Semaphore)去实现。

理解 AQS,不仅仅是为了应付面试,更是为了学习这种优雅的架构设计思维。当我们下次再使用 CountDownLatch 或 ReentrantLock 时,不再只是看到了一个黑盒,而是能透过 API 看到底层那个井然有序的队列和原子变动的 State。