AQS模板方法
一、AQS 是什么?
AbstractQueuedSynchronizer
(简称 AQS)是 Java 并发包 java.util.concurrent.locks
中的一个抽象类,它是构建锁和同步器(如 ReentrantLock
, Semaphore
, CountDownLatch
等)的核心框架。
你可以把 AQS 看作一个**“同步状态管理器”**。它解决了并发编程中最核心的问题:
- 如何管理一个共享的状态?(比如锁的“被持有”或“未被持有”状态,信号量的“可用许可数”)。
- 当多个线程竞争这个状态时,如何让竞争失败的线程排队等待?
- 当状态被释放时,如何从等待队列中唤醒一个或多个线程?
AQS 将这些通用的、复杂的线程排队、状态管理、唤醒机制都封装好了,而把“状态是什么”以及“在什么条件下可以获取/释放状态”这个问题留给了它的子类去定义。
二、模板方法模式
在深入 AQS 之前,我们先快速回顾一下模板方法模式。
1. 定义
模板方法模式在一个方法中定义了一个算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
2. 两个关键角色
- 模板方法:在父类中定义,它是一个具体方法,包含了算法的骨架。它调用一系列基本方法(可以是抽象的,也可以是具体的)来完成整个流程。
- 基本方法:
- 抽象方法:由父类声明,子类必须实现。这是算法中可变的部分。
- 具体方法:由父类实现,子类可以继承或覆盖。这是算法中不变的部分。
3. 简单例子
// 父类:定义制作饮料的流程
abstract class BeverageTemplate {// 这是模板方法,定义了制作饮料的算法骨架(final防止子类覆盖)public final void prepareBeverage() {boilWater(); // 固定步骤brew(); // 可变步骤,由子类实现pourInCup(); // 固定步骤addCondiments(); // 可变步骤,由子类实现}private void boilWater() {System.out.println("Boiling water");}private void pourInCup() {System.out.println("Pouring into cup");}// 抽象方法,由子类实现protected abstract void brew();protected abstract void addCondiments();
}// 子类1:制作咖啡
class Coffee extends BeverageTemplate {@Overrideprotected void brew() {System.out.println("Dripping Coffee through filter");}@Overrideprotected void addCondiments() {System.out.println("Adding Sugar and Milk");}
}// 子类2:制作茶
class Tea extends BeverageTemplate {@Overrideprotected void brew() {System.out.println("Steeping the tea");}@Overrideprotected void addCondiments() {System.out.println("Adding Lemon");}
}
在这个例子中,prepareBeverage()
就是模板方法,它定义了制作饮料的通用流程。而 brew()
和 addCondiments()
就是抽象的基本方法,由子类(Coffee
, Tea
)来提供具体实现。
三、AQS 如何运用模板方法模式
AQS 是模板方法模式在 Java 并发领域最经典、最完美的应用。
1. AQS 中的“模板方法”
AQS 内部提供了一系列对使用者开放的、公有的、final 的方法,这些方法就是模板方法。它们定义了获取和释放同步状态的完整流程。
例如:
acquire(int arg)
:独占模式下获取同步状态。如果获取失败,线程会进入等待队列。release(int arg)
:独占模式下释放同步状态。如果释放成功,会唤醒等待队列中的第一个线程。acquireShared(int arg)
:共享模式下获取同步状态。releaseShared(int arg)
:共享模式下释放同步状态。acquireInterruptibly(int arg)
:可中断的获取。
这些方法都是 final
的,意味着任何继承 AQS 的子类都不能去修改它们的核心流程。这些流程(比如排队、阻塞、唤醒)已经被 AQS 精心设计并固化了。
2. AQS 中的“基本方法”(钩子方法)
AQS 将“判断是否可以获取/释放状态”这个核心逻辑,抽象成了一系列受保护的、抽象的或可覆盖的方法,供子类实现。这些就是基本方法或钩子方法。
核心的钩子方法主要有以下几个:
方法名 | 描述 | 模式 |
---|---|---|
tryAcquire(int arg) | 尝试以独占模式获取状态。如果成功,返回 true 。这是实现 ReentrantLock 等独占锁的核心。 | 独占 |
tryRelease(int arg) | 尝试以独占模式释放状态。如果释放后允许唤醒后继节点,返回 true 。 | 独占 |
tryAcquireShared(int arg) | 尝试以共享模式获取状态。返回值大于等于0表示成功,负数表示失败。这是实现 Semaphore 、CountDownLatch 的核心。 | 共享 |
tryReleaseShared(int arg) | 尝试以共享模式释放状态。如果释放后可能需要唤醒后继节点,返回 true 。 | 共享 |
isHeldExclusively() | 查询当前同步器是否被当前线程独占。Condition 的实现需要它。 | 独占 |
关键点:这些方法在 AQS 中默认会抛出 UnsupportedOperationException
。AQS 强制子类必须根据自己想要的同步语义(是独占锁还是共享锁)去实现它们。
3. AQS 的“状态”——state
变量
AQS 内部维护了一个 private volatile int state
变量。这个 state
就是同步状态,它的具体含义完全由子类来定义。
- 对于
ReentrantLock
,state
可以表示锁被重入的次数。0 表示未被持有,1 表示被持有一次,N 表示被重入 N 次。 - 对于
Semaphore
,state
表示剩余的可用许可数。 - 对于
CountDownLatch
,state
表示需要倒计数的初始值。
AQS 提供了 getState()
, setState(int newState)
, compareAndSetState(int expect, int update)
这三个 final
方法来安全地操作 state
,子类通过这些方法来管理状态。
四、实例解析:ReentrantLock
如何基于 AQS 实现
ReentrantLock
内部有一个 Sync
类,它继承自 AQS
。
1. 定义状态
ReentrantLock
将 AQS 的 state
定义为锁的持有计数。
2. 实现钩子方法
ReentrantLock
有公平锁和非公平锁两种模式,它们分别对应 Sync
的两个子类 FairSync
和 NonfairSync
。我们以非公平锁为例,看看它是如何实现 tryAcquire
的。
// ReentrantLock.NonfairSync 中的简化版 tryAcquire
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // 获取当前状态(持有次数)if (c == 0) { // 如果状态为0,表示锁未被持有// 使用 CAS 尝试将状态从 0 设置为 acquires(通常是1)if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current); // 如果成功,设置当前线程为独占者return true; // 获取成功}}// 如果状态不为0,检查持有锁的线程是不是自己else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires; // 如果是,增加重入次数if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc); // 直接设置状态,因为已经是独占状态,不需要CASreturn true; // 重入成功}// 否则,获取失败return false;
}
分析:
- 这个
nonfairTryAcquire
方法就是 AQS 的tryAcquire
钩子方法的具体实现。 - 它定义了“非公平地获取一个可重入锁”的业务逻辑:先抢一下(CAS),抢不到再看是不是自己的锁(重入)。
- 它只关心“能不能拿到锁”,至于拿不到怎么办(排队、阻塞),它完全不关心。
3. 调用流程
当你的代码调用 lock.lock()
时:
lock()
方法会调用 AQS 的模板方法acquire(1)
。acquire(1)
(模板方法)的内部逻辑大概是:
public final void acquire(int arg) {if (!tryAcquire(arg) && // 调用子类实现的 tryAcquireacquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果失败,则入队并阻塞selfInterrupt();}
- AQS 的
acquire
方法首先调用tryAcquire(1)
,也就是我们上面看到的nonfairTryAcquire(1)
。 - 如果
tryAcquire
返回true
,acquire
方法执行完毕,线程成功获取锁,继续执行。 - 如果
tryAcquire
返回false
,AQS 就会接管后续工作:将当前线程包装成一个节点(Node
)加入到内部的 CLH 等待队列中,然后通过LockSupport.park()
将线程挂起(阻塞)。
这就是模板方法模式的精髓:AQS 定义了“获取锁”这个复杂流程的骨架(acquire
),而将“如何判断锁是否可用”这个核心逻辑(tryAcquire
)留给子类(ReentrantLock
)去实现。
五、总结
特性 | 描述 |
---|---|
AQS 角色 | 模板方法模式中的抽象父类。它定义了同步器的通用算法骨架。 |
模板方法 | acquire() , release() , acquireShared() 等。它们是 final 的,定义了获取/释放状态的完整流程,包括排队、阻塞、唤醒等不变逻辑。 |
基本方法(钩子) | tryAcquire() , tryRelease() , tryAcquireShared() 等。它们是 protected 的,由子类实现,定义了“状态是否可获取/释放”的可变逻辑。 |
核心状态 | volatile int state 。其具体含义由子类定义,AQS 只提供安全的访问方法(getState , setState , CAS )。 |
设计优势 | 解耦和复用。AQS 将复杂的并发控制逻辑与具体的同步语义分离开。开发者只需关注自己的业务逻辑(如何定义状态和获取/释放条件),而无需关心底层的线程排队和调度,极大地简化了高性能同步器的开发难度。 |
通过这种设计,Java 并发包能够以 AQS 为基础,高效地构建出 ReentrantLock
、Semaphore
、CountDownLatch
、ReentrantReadWriteLock
等功能各异但底层机制统一的同步工具,是 Java 并发库设计的基石。