精选·Java与并发

AQS 原理与 ReentrantLock 实现剖析

91学AI·2026/7/13·6 阅读

考察点

AQS(AbstractQueuedSynchronizer)是 JUC 包的地基,能讲清它基本就通过了并发进阶面。面试官想看你是否理解「一个 volatile 状态 + 一个 FIFO 等待队列 + CAS」怎么撑起 ReentrantLock、Semaphore、CountDownLatch 这一整套工具。追问常往公平锁和非公平锁差异、CLH 队列细节、Condition 的实现、AQS 和 synchronized 的对比走。

参考答案

AQS 的三大核心组件

state 状态:一个 volatile int,语义由子类定义。ReentrantLock 里它表示锁的重入次数;Semaphore 里表示剩余许可数;CountDownLatch 里表示剩余计数。所有并发控制都收敛为对这个 int 的 CAS 操作。

CLH 变体等待队列:一个双向链表构成的 FIFO 队列,没抢到锁的线程被包装成 Node 入队挂起。队列有几个细节值得讲:头节点是个不持有线程的「空节点」(dummy),真正的等待者从第二个节点开始;每个节点有 waitStatus 状态字段(CANCELLED、SIGNAL、CONDITION、PROPAGATE);节点入队用 CAS 改 tail,出队和唤醒由 head 驱动。SIGNAL 的含义是「后继节点需要我唤醒」,保证了唤醒信号沿队列传播。

模板方法模式:AQS 把获取/释放的流程骨架写死(acquire、release 是 final 的),子类只实现 tryAcquire、tryRelease、tryAcquireShared 这几个钩子方法,定义「怎么改 state」。这是 AQS 设计最精妙的地方——排队、挂起、唤醒这些易错的细节被封装掉,并发工具作者只需要关心状态语义。

独占模式的获取流程

以 acquire 为例,非公平锁的标准路径:

  1. 先 tryAcquire 试一次(CAS 抢 state),成功就直接拿锁。
  2. 失败则 addWaiter 把当前线程包装成 Node,CAS 追加到队尾。
  3. acquireQueued 里自旋:如果前驱是头节点,再 tryAcquire 一次(头节点的后继是第一个有资格竞争的);失败且前驱状态被设为 SIGNAL 后,用 LockSupport.park 挂起线程。
  4. 被唤醒后重新竞争,拿到锁就把自己设为头节点,清理已取消的节点。

「自旋 + park 挂起」的结合很关键:刚入队的线程先自旋抢一两次,赌持锁线程很快释放;抢不到才挂起,避免无意义地烧 CPU。这和 synchronized 轻量级锁的自旋思路一脉相承。

ReentrantLock 的实现

非公平锁(默认):tryAcquire 时不管队列里有没有人排队,直接 CAS 抢 state。新来线程可能在刚释放的瞬间插队成功。吞吐量高——因为省去了线程唤醒的开销,但可能导致排队线程长时间饥饿。

公平锁:tryAcquire 前先用 hasQueuedPredecessors 检查队列里有没有更早的等待者,有就老实排队。严格 FIFO,吞吐略低,但避免饥饿。

可重入:state 从 0 CAS 到 1 拿锁,同一线程再次进入 state 加 1,同时记录 exclusiveOwnerThread 指向自己;unlock 时 state 减 1,减到 0 才真正释放。重入次数和释放次数必须配对,否则锁永远放不掉。

共享模式

acquireShared 和独占模式的差别在于「传播」:一个线程获取成功后,会把唤醒信号向后传播,让后续节点继续尝试——这就是 PROPAGATE 状态的用途。CountDownLatch 计数归零时所有等待线程被一次性唤醒、Semaphore 释放一个许可可能唤醒多个等待者,靠的都是传播机制。

Condition 与工程实践

ReentrantLock 的 newCondition 返回的是 AQS 内部的 ConditionObject,每个 Condition 有自己的条件队列(和同步队列分离)。await 时线程从同步队列挪到条件队列并释放锁;signal 时挪回同步队列重新竞争。比 synchronized 的 wait/notify 强在可以按条件分组唤醒——比如有界队列的 notFull 和 notEmpty 两个条件,生产者只唤醒消费者,不会惊群。AQS 框架下还长出了 ReentrantReadWriteLock(高低 16 位拆 state)、StampedLock(乐观读),读懂 AQS 这些都能举一反三。

可能的追问

  • 为什么 AQS 用双向链表,CLH 原版不是单向的吗? AQS 是 CLH 的变体,双向链表为了支持取消(CANCELLED 节点要从链上摘除)和从后往前找有效的未取消前驱。
  • 非公平锁为什么吞吐更高? 线程释放锁到唤醒等待线程之间有个时间窗,这期间新来的线程直接抢到锁就不用经历「挂起-唤醒」一轮上下文切换,整体吞吐更高,代价是可能饥饿。
  • AQS 和 synchronized 怎么选? synchronized 简单场景够用且有 JVM 优化;需要可中断、超时、公平性、多条件队列、共享模式时用 AQS 系工具。底层一个靠 monitor(JVM 层),一个靠 CAS+LockSupport(Java 层)。
  • CountDownLatch 和 CyclicBarrier 在 AQS 上怎么实现? CountDownLatch 用共享模式,state 是计数,await 的线程在共享队列排队,countDown 减到 0 时传播唤醒全部;CyclicBarrier 没用 AQS,是 ReentrantLock + Condition 实现的分代(generation)重置。

评论 (0)

暂无评论,快来抢沙发吧!

91学AI

© 2026 91学AI · 按岗位学 AI 与大数据. All rights reserved.