跳到主要内容
约 7 分钟3 张卡

AQS

AbstractQueuedSynchronizer(AQS)是实现阻塞锁和同步器的框架。子类规定资源怎样获取和释放,AQS 处理获取失败后的排队与等待。本文以 Java 17/21 为准,从同步状态入手,再说明等待队列、独占与共享模式。最后解释取消、条件等待和线程唤醒的关系。

1. 面试核心​

先记住这几条,再读正文
  • 同步状态。 AQS 用一个 int state 表示同步状态,子类决定它代表重入次数、剩余计数还是可用许可。
  • 排队与唤醒。 AQS 在子类尝试获取失败后管理排队、阻塞和取消,资源释放后再唤醒有效后继。卡 · 同步状态 state
  • 共享获取结果。 tryAcquireShared 返回负数表示失败,返回零表示本次成功但后续获取无法成功,返回正数表示后续仍有成功机会。卡 · 共享获取的返回值
  • 线程许可。 LockSupport 为每个线程维护至多一个许可,unpark 提供许可,park 消耗许可或等待。卡 · park 与 unpark 的许可

2. 同步状态 state​

同步器需要记录资源是否可用,AQS 用一个 int state 保存这个状态。子类通过下面的方法读取或修改它。

getState() 和 setState() 遵循 volatile 的可见性与顺序约束。compareAndSetState() 则原子地比较并更新状态,避免读取后状态已变却仍按旧值写入。

getState();
setState(next);
compareAndSetState(expected, next);

同一个整数在不同同步器中代表不同含义。可以对照以下三种同步状态理解:

  • ReentrantLock 用它记录当前线程的持有次数。同一线程重复获取这把锁时,计数会加一。
  • CountDownLatch 用它记录剩余计数,计数归零后允许等待线程继续执行。
  • Semaphore 用它记录可用许可,线程取得许可后才能使用受限资源。

子类通过 tryAcquire、tryRelease 或共享模式的对应方法定义状态转换。AQS 的模板方法则规定获取和等待的整体流程。比如,模板先调用子类尝试获取,失败后再安排线程排队。

3. FIFO 等待队列​

资源暂时不可用时,AQS 用 FIFO 队列组织等待线程。FIFO 指先进入的节点排在前面,线程通过队列等待再次尝试获取。

以独占获取为例,可以沿着以下获取流程理解:

  1. 子类通过 tryAcquire 尝试获取状态。
  2. 获取失败后,线程节点进入等待队列。
  3. 节点到达合适位置后重新尝试获取。暂时无法继续时,线程通过 LockSupport.park 暂停执行。
  4. 释放方更新状态,并在满足唤醒条件时唤醒有效后继,也就是尚未取消的后续等待者。
  5. 后继恢复执行后,再次尝试获取状态。

队列按 FIFO 组织等待者,但获取前会先尝试调用子类的方法。这时,新来的线程仍有机会抢先取得资源,所以公平性取决于子类的获取策略。公平实现可用 hasQueuedPredecessors() 检查是否已有线程排在前面,再决定是否获取。

4. 独占模式与共享模式​

同步器需要决定资源能由多少线程同时获取。独占模式一次允许一个持有者,比如互斥锁。共享模式则允许多个线程在条件满足时成功获取资源。两种模式的等待者使用同一条同步队列。

共享获取成功后,AQS 会让后继继续检查是否满足获取条件,这称为共享传播。比如,CountDownLatch 归零后,全部等待者都可以继续执行。Semaphore(3) 则只有三个初始许可。每个线程占用一个许可时,最多允许三个线程同时使用资源。后继能否成功,仍取决于剩余许可是否足够。

5. 中断、超时与取消​

等待资源时,调用方还要决定是否响应中断,以及最多等待多久。AQS 提供以下三类获取模板:

  • acquire 在等待期间继续获取,即使收到中断也会继续等待。获取成功返回时,它会保留或恢复中断标志。
  • acquireInterruptibly 响应中断,退出等待并抛出 InterruptedException。
  • tryAcquireNanos 同时支持中断和超时,超时仍未取得资源时返回 false。

等待者因中断或超时取消后,队列需要跳过其节点。这样,释放方才能找到仍在等待的有效后继。排队和取消都在并发发生,所以 getQueueLength() 只返回等待线程数的估计值。监控代码应结合多次采样判断积压。

6. Condition 条件队列​

等待业务条件与等待锁是两种不同的等待。比如,消费者已经持有锁,却发现队列为空,这时需要等待生产者放入元素。AQS 的 ConditionObject 用独立的条件队列保存这类等待者。

条件等待包含以下队列转移过程:

  1. 线程调用 await 后,节点进入条件队列,并完全释放已持有的同步状态。
  2. 其他线程持有同一把锁调用 signal,将等待节点转入同步队列。
  3. 等待线程重新取得同步状态后,才能从等待中返回并继续执行。

因此,signal 发出通知后,等待线程仍要重新竞争锁。子类需要支持独占持有检查,也要能完全释放并恢复原有持有状态。

一把 ReentrantLock 可创建多个 Condition,分别等待“非空”和“未满”,减少无关唤醒。使用方式见 ReentrantLock。

7. AQS 的适用范围​

AQS 适合用一个 int 管理同步状态、获取失败后需要排队阻塞的组件。子类也可维护锁所有者等辅助字段,但 AQS 跟踪的同步状态仍是这个整数。自定义获取和释放方法应短小、线程安全,并避免阻塞。

业务代码可以直接使用 JUC,也就是 java.util.concurrent 包提供的并发组件。比如,互斥使用 ReentrantLock,限制并发数使用 Semaphore,等待计数归零使用 CountDownLatch。

自行扩展 AQS 时,需要验证以下协议边界:

  • 状态转换是否始终满足资源的使用规则。
  • 获取和释放是否正确检查所有权。
  • 中断、超时与取消是否让等待者正确退出。
  • 序列化后是否能恢复可用状态。AQS 自身只保存同步状态,反序列化后的等待队列为空。

8. 共享获取的返回值​

共享获取既要告诉 AQS 本次是否成功,也要说明后续是否还有获取机会。因此,tryAcquireShared(arg) 用以下三个返回区间表达结果:

返回值含义
负数本次获取失败
零本次成功,后续共享获取无法成功
正数本次成功,后续共享获取仍有成功机会

返回正数表示后继仍有成功机会,具体数值无需等于剩余许可数。后继真正执行时,其他线程也会竞争资源,所以它仍要重新检查状态。释放共享资源时,tryReleaseShared 返回 true 会触发后续唤醒。

9. park 与 unpark 的许可​

AQS 通过 LockSupport 暂停和唤醒线程。它为每个线程维护至多一个许可,用来协调 park 与 unpark。

unpark(thread) 为目标线程提供许可,已有许可时仍只保留一个。线程调用 park 时,有许可就消耗它并返回,否则进入等待。因此,目标线程已启动且许可未被消耗时,先 unpark 再 park 也能配对。

park 还会因中断或虚假唤醒返回。虚假唤醒指没有收到对应通知,等待也自行结束的情况。park 返回时会保留中断标志,调用者需要自行检查。因此,等待代码要在循环中重查条件,确认资源可用后再继续。

10. 常见问题​

10.1 AQS 与 CLH 队列​

AQS 借鉴了 CLH 队列锁组织等待者的方式。经典 CLH 自旋锁由等待者反复读取前驱状态,判断自己能否继续。AQS 则结合 park/unpark 实现阻塞等待,并支持取消、独占和共享传播。因此,分析 AQS 时还需要解释这些等待协议。

10.2 唤醒与锁获取​

11 · 面试题卡片​

11.1 AQS 如何组织获取失败的线程​

出现公司:蚂蚁集团、阅文集团

考察重点

  • 说明子类怎样定义 state,以及获取和释放的条件。
  • 解释获取失败的线程怎样排队、暂停,并在唤醒后重试。
  • 区分独占与共享获取,并说明取消节点为何需要被跳过。

相关内容:同步状态 state至中断、超时与取消。

参考回答

AQS 把获取失败的线程加入 FIFO 等待队列,并安排重试、阻塞和唤醒。

子类先通过 tryAcquire 等方法尝试获取资源。排队后,线程在合适位置重试,暂时无法继续时通过 park 等待。释放方更新状态并唤醒有效后继,后继恢复后再竞争资源。

子类定义 state 的含义和转换条件,AQS 管理独占与共享等待。可中断和超时获取让等待者退出,队列随后跳过取消节点。公平策略则由子类检查前驱等方式实现,因为新线程在入队前也会尝试获取。

11.2 tryAcquireShared 的三个返回区间分别表示什么?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:区分本次获取成功与后继仍可获取,并解释后继为何需要重新检查状态。

相关内容:共享获取的返回值。

参考回答

负数表示本次失败,零表示本次成功但后续无法成功,正数表示本次成功且后续仍有成功机会。

正数只是说明还有获取机会,数值无需等于剩余许可数。后继执行时,资源还会被其他线程竞争,所以它必须重新检查状态。

11.3 先调用 unpark 再调用 park 会丢失唤醒吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释许可为何最多积累一个,以及中断和虚假唤醒为何要求循环检查条件。

相关内容:park 与 unpark 的许可。

参考回答

目标线程已启动且许可未被消耗时,先调用 unpark 会保留许可。

下一次 park 消耗这个许可后直接返回,因此这次唤醒可以先于等待发生。

连续两次 unpark 仍只积累一个许可。此外,park 会因中断或虚假唤醒返回,所以返回后仍需循环检查资源条件。

12. 参考资料​