以ReentrantLock为例解释AQS的工作流程
AQS及其子类的协作
思考:AQS是怎么利用模板方法完成工作流程的,AQS做了哪些;如果子类要具体实现,子类有需要完成哪些功能或方法?
AQS(AbstractQueuedSynchronizer)是整个JUC包同步器的基石。它使用模板方法模式,把复杂的线程排队、阻塞、唤醒等通用流程固化在父类中,而把“资源是否可用”的判断逻辑交给子类去决定。
这是一个非常经典的“你定规则,我管排队”的分工协作。
👑 AQS的模板方法:定义不可变的算法骨架
AQS提供了一系列 public final 方法作为模板方法(例如 acquire()、release()),这些方法定义了获取或释放资源的整个操作流程,并且不允许子类覆盖,以保证线程调度逻辑的绝对安全。
以独占式获取锁的模板方法 acquire(int arg) 为例,它封装了一个完整的获取锁流程:
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 1. 尝试获取锁,具体逻辑由子类实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg) // 2. 获取失败,则包装线程并入队等待
)
selfInterrupt(); // 3. 如果在等待过程中被中断,则自我中断
}
在这个流程中,tryAcquire(arg) 就是那个由子类实现的“钩子方法”。AQS定义好了“骨架”,子类只需填入核心逻辑即可。
🏢 类比理解:银行办事大厅
我们可以把AQS的工作机制比作一个银行办事大厅,这会非常直观:
- state (同步状态):大厅里的空闲柜台数量,是一个
volatile int变量。AQS提供了getState(),setState(),compareAndSetState()三个原子方法来操作它。 - CLH队列 (同步队列):大厅里用于排队的等候区。当柜台被占满,没抢到柜台的客户(线程)会被封装成一个
Node节点,加入到一个双向队列的尾部等待叫号。 - ConditionObject (条件队列):大厅里的VIP休息室。当某些业务无法办理时(如从ATM取钱,但现金不足),客户会去条件队列等待,直到条件满足再被唤醒,重新回到CLH队列排队。
🧱 AQS做了什么?—— 封装了“等待”与“通知”的复杂细节
AQS的核心工作,就是封装了上面比喻中管理“排队叫号”这种繁琐但通用的流程,它主要做了以下几件事:
- 负责管理
state:安全地维护一个核心的volatile int state,并提供原子操作方法。 - 维护同步队列(CLH队列):当线程获取锁失败时,自动将其包装并安全地加入队列,同时管理队列的入队和出队。
- 处理线程阻塞与唤醒:当线程在队列中排队时,精准地将其
park阻塞;当资源被释放时,再精准地unpark唤醒下一个等待的线程。
🧩 子类需要做什么?—— 聚焦于“资源是否可用”的业务判断
AQS规定了5个可以被重写的方法,子类通过实现这些方法来定义自己的同步语义。这些方法的核心职责就是安全地判断并修改state的值,以判断资源是否可用。
以下是需要实现的几个关键方法:
| 方法 | 适用模式 | 你的核心任务 |
|---|---|---|
tryAcquire(int arg) |
独占模式 | 尝试获取资源。根据state的值判断资源是否空闲。若是,则通过CAS原子性地更新state,并返回true;否则返回false。 |
tryRelease(int arg) |
独占模式 | 尝试释放资源。将state修改回空闲状态,通常需要保证只有持有锁的线程才能释放,并返回true。 |
tryAcquireShared(int arg) |
共享模式 | 尝试获取共享资源。返回值语义更丰富:负数表示失败;0表示成功但无剩余资源;正数表示成功且还有剩余资源。 |
tryReleaseShared(int arg) |
共享模式 | 尝试释放共享资源。将state增加,表示资源变多,并返回true。 |
isHeldExclusively() |
独占模式 | 判断当前线程是否独占资源。主要用于ConditionObject,以判断当前线程是否持有锁。 |
💎 总结
AQS通过模板方法模式,将通用的线程调度逻辑与具体的资源获取逻辑分离开来。子类不再需要关心复杂的线程管理,只需专注于实现上述5个钩子方法,通过修改state来定义自己的同步语义。这正是Java并发包灵活性与强大扩展能力的基石。
结合ReentrantLock说明
核心思考:AQS做了什么,ReentrantLock做了什么,他们是怎么协作工作的
下面以 ReentrantLock 为具体例子,重新梳理 AQS 和 ReentrantLock 各自做了什么,以及它们如何协作完成锁机制。
一、AQS 做了什么 —— “排队管理器”
AQS(AbstractQueuedSynchronizer)是一个同步队列 + 阻塞/唤醒的通用框架,它封装了:
- 一个
volatile int state状态变量:表示资源数量。- 在 ReentrantLock 中,
state表示锁的重入次数:0表示无锁;>0表示被某线程持有,且值为重入次数。
- 在 ReentrantLock 中,
- 一个双向 CLH 队列(同步队列):存放等待获取锁的线程。
- 线程的阻塞(
LockSupport.park)与唤醒(unpark)。 - 条件队列(
ConditionObject):实现await/signal的基础。 - 模板方法:
acquire(),release(),acquireShared(),releaseShared()等,定义好“获取资源失败则入队阻塞,释放资源则唤醒后继”的标准流程。
核心思想:AQS 只负责“如果资源不够,该怎么排队、阻塞、唤醒”,而资源是否够的判断逻辑由子类(同步器)通过钩子方法提供。
二、ReentrantLock 做了什么 —— “锁语义实现者”
ReentrantLock 是一个可重入的独占锁。它没有从零实现排队阻塞,而是内部组合了一个继承 AQS 的同步器(Sync),并利用 AQS 的能力,只实现以下“判断逻辑”:
- 定义
state的含义:0表示未锁定,>0表示当前线程重复获取的次数。 - 实现
tryAcquire(int acquires):尝试直接获取锁。- 检查
state。 - 如果
state==0,尝试 CAS 设置state=acquires,并记录当前持有线程为自身 → 成功。 - 如果
state>0且当前线程就是持有线程 → 增加state值 → 成功(重入)。 - 否则 → 失败。
- 检查
- 实现
tryRelease(int releases):尝试释放锁。- 计算新的
state = state - releases。 - 如果新
state == 0,说明完全释放,清除持有线程记录,返回true; - 否则返回
false(仍被当前线程重入持有)。
- 计算新的
- 实现
isHeldExclusively():判断锁是否被当前线程独占。 - 提供公平/非公平策略:通过不同的
tryAcquire实现。- 非公平:
tryAcquire中一上来就直接 CAS 抢锁,不管同步队列中是否已有等待线程。 - 公平:
tryAcquire中先检查同步队列中是否有前驱节点(hasQueuedPredecessors()),若有则自己排队,避免插队。
- 非公平:
注意:ReentrantLock 的 lock() 和 unlock() 方法,其实只是调用了内部 Sync 的 acquire(1) 和 release(1),而这两个方法是 AQS 定义好的模板方法。
三、它们如何协同工作 —— 以 lock() 为例
我们以 非公平锁 为例,画出完整流程:
1. 调用 ReentrantLock.lock()
public void lock() {
sync.acquire(1); // sync 是继承 AQS 的子类实例
}
2. 进入 AQS 的 acquire(int arg) 模板方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && // ① 尝试获取锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { // ② 失败,入队等待
selfInterrupt(); // ③ 自我中断
}
}
3. 调用钩子方法 tryAcquire(1)(由 ReentrantLock 实现)
非公平策略下:
- 立即读取
state,如果state == 0,则尝试 CAS 设置state = 1并设置独占线程为当前线程 → 成功则直接获得锁,tryAcquire返回true,acquire方法结束。 - 如果
state > 0且当前线程就是持有线程,则state++,重入成功,返回true。 - 否则返回
false。
4. 如果 tryAcquire 返回 false(获取锁失败)
addWaiter:将当前线程包装成一个Node节点,并加入同步队列的尾部。acquireQueued:进入自旋。- 判断当前节点的前驱是否为 head(即自己是否是队列中第一个等待者)。
- 如果是,再次调用
tryAcquire(1)尝试获取锁(又给了一次机会)。 - 如果获取成功,则设置该节点为新的 head,并返回。
- 如果获取失败,则检查是否应该阻塞(
shouldParkAfterFailedAcquire),然后调用park()阻塞当前线程。
5. 当锁被释放时:调用 ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
进入 AQS 的 release(int arg) 模板方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒 head 的后继节点
return true;
}
return false;
}
tryRelease(1)由 ReentrantLock 实现:减少state,如果state变为0,清除独占线程,返回true。- 当
tryRelease返回true后,AQS 会唤醒同步队列中的下一个线程,使其继续执行acquireQueued中的自旋尝试获取锁。
四、公平与非公平的具体差异(在 tryAcquire 中)
| 策略 | tryAcquire 实现核心 |
|---|---|
| 非公平 | if (state == 0 && CAS set state to 1) success → 直接抢,无视队列 |
| 公平 | if (state == 0 && !hasQueuedPredecessors() && CAS success) → 队列有等待者则自己排队 |
其余所有排队、阻塞、唤醒逻辑全部复用 AQS 代码,一个字都不用改。
五、总结
| 模块 | 负责的内容 | 关键代码/方法 |
|---|---|---|
| AQS | 同步队列维护、线程 park/unpark、状态原子更新、模板流程(acquire, release) |
acquire(), release(), addWaiter(), acquireQueued(), unparkSuccessor() |
| ReentrantLock | 锁的语义(可重入、公平/非公平)、实现 tryAcquire/tryRelease 来操作 state、记录独占线程 |
NonfairSync.tryAcquire(), FairSync.tryAcquire(), Sync.tryRelease() |
一句话:AQS 负责“如何排队”,ReentrantLock 负责“什么情况下可以插队/重入”。两者通过模板方法模式完美解耦,这也是 JUC 中 Semaphore, CountDownLatch, ReentrantReadWriteLock 等工具都基于 AQS 的原因。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)