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