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的核心工作,就是封装了上面比喻中管理“排队叫号”这种繁琐但通用的流程,它主要做了以下几件事:

  1. 负责管理state:安全地维护一个核心的volatile int state,并提供原子操作方法。
  2. 维护同步队列(CLH队列):当线程获取锁失败时,自动将其包装并安全地加入队列,同时管理队列的入队和出队。
  3. 处理线程阻塞与唤醒:当线程在队列中排队时,精准地将其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 为具体例子,重新梳理 AQSReentrantLock 各自做了什么,以及它们如何协作完成锁机制。


一、AQS 做了什么 —— “排队管理器”

AQS(AbstractQueuedSynchronizer)是一个同步队列 + 阻塞/唤醒的通用框架,它封装了:

  1. 一个 volatile int state 状态变量:表示资源数量。
    • 在 ReentrantLock 中,state 表示锁的重入次数0 表示无锁;>0 表示被某线程持有,且值为重入次数。
  2. 一个双向 CLH 队列(同步队列):存放等待获取锁的线程。
  3. 线程的阻塞(LockSupport.park)与唤醒(unpark
  4. 条件队列(ConditionObject:实现 await/signal 的基础。
  5. 模板方法acquire(), release(), acquireShared(), releaseShared() 等,定义好“获取资源失败则入队阻塞,释放资源则唤醒后继”的标准流程。

核心思想:AQS 只负责“如果资源不够,该怎么排队、阻塞、唤醒”,而资源是否够的判断逻辑由子类(同步器)通过钩子方法提供。


二、ReentrantLock 做了什么 —— “锁语义实现者”

ReentrantLock 是一个可重入的独占锁。它没有从零实现排队阻塞,而是内部组合了一个继承 AQS 的同步器Sync),并利用 AQS 的能力,只实现以下“判断逻辑”:

  1. 定义 state 的含义0 表示未锁定,>0 表示当前线程重复获取的次数。
  2. 实现 tryAcquire(int acquires):尝试直接获取锁。
    • 检查 state
    • 如果 state==0,尝试 CAS 设置 state=acquires,并记录当前持有线程为自身 → 成功。
    • 如果 state>0 且当前线程就是持有线程 → 增加 state 值 → 成功(重入)。
    • 否则 → 失败。
  3. 实现 tryRelease(int releases):尝试释放锁。
    • 计算新的 state = state - releases
    • 如果新 state == 0,说明完全释放,清除持有线程记录,返回 true
    • 否则返回 false(仍被当前线程重入持有)。
  4. 实现 isHeldExclusively():判断锁是否被当前线程独占。
  5. 提供公平/非公平策略:通过不同的 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 返回 trueacquire 方法结束。
  • 如果 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 的原因。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐