说实话接触过一段时间并发编程的人迟早都会撞上一个叫AbstractQueuedSynchronizer的类。它在java.util.concurrent.locks包里平时你几乎不会直接用它但ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock甚至线程池里的Worker底层都在围着它转。圈子里一直叫它 AQS也有人说它是并发包的“珠穆朗玛峰”。这个比喻挺贴切的——它不是代码量多到吓人而是抽象层次高到离谱你盯着源码半天未必看得懂一行代码究竟在干嘛。这篇文章我想换个思路不按源码顺序一行行讲而是先讲清楚 AQS 到底是干什么的、为什么这么设计再带着你手写一个基于 AQS 的锁最后把加锁解锁、条件队列这些核心流程彻底拆明白。顺带把面试高频问题、线上线程卡死怎么排查也一起收拾了。适合正在啃并发源码的人也适合准备面试的工程师看完你至少能跟别人把 AQS 聊明白。1. 先搞清楚 AQS 在并发包里的真实地位1.1 所有同步工具背后那个“隐形地基”先做一个最简单的类比。你去餐厅吃饭店里有一套通用的排号系统取号、等位、叫号、入座。餐厅自己不用关心客人在哪儿排队、排了多久、座位满了怎么处理这套系统全包了。AQS 就是 Java 并发世界里的那套“排号系统”只不过它排的不是座位是锁和资源。它的官方定位很明确一个用于构建锁和同步器的框架。你不需要直接继承它去写业务代码而是在实现自定义同步器时继承它然后重写几个钩子方法告诉它“什么样的状态算获取成功、什么样的状态算释放成功”。至于线程拿不到锁之后怎么排队、怎么挂起、怎么唤醒这些通用流程 AQS 自己全处理了。这就是它最牛的地方把并发工具里最复杂、最容易出错的“线程阻塞与唤醒管理”收拢成了一套通用引擎。你想想ReentrantLock 加锁失败要排队Semaphore 获取信号量失败要排队CountDownLatch 等待计数归零也要排队。如果每个工具都自己写一套排队逻辑不出 bug 才怪。AQS 把“排队”这件事做成了标准件大家只需要关心自己的业务状态。1.2 AQS 到底难在哪抽象程度极高的设计很多第一次看 AQS 源码的人都会懵包括我自己。类声明就一行public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable。不算太复杂可里面每一行都带着好几层意思。难点之一是它完全没用synchronized同步全靠volatile变量配合 CAS 操作来完成。这意味着你要同时理解内存可见性、原子操作、线程调度这些底层概念才能看懂它为什么这样写。难点之二是它的方法在不同场景下语义完全不同。比如state在 ReentrantLock 里表示“重入次数”在 Semaphore 里表示“剩余许可数”在 CountDownLatch 里表示“还需倒数几个事件”。同一个字段换个子类就是另一种含义。还有一个容易让人绕晕的地方AQS 同时支持独占模式和共享模式。独占模式像“一把钥匙只能给一个人”共享模式像“一车座位可以坐很多人”。这两套流程在代码里互相独立但都挂在同一个框架下还有 Condition 条件队列插一脚。所以你读源码时总有一种感觉这不是在看一个锁是在看一整类锁的抽象集合。2. AQS 核心机制拆解state、队列、模板方法2.1 state 变量同步状态的唯一源头一切都要从private volatile int state这个字段说起。AQS 把所有的同步状态都浓缩成了这一个整数。为什么用 int 不用 long因为在绝大多数场景下一个 int 的取值范围已经足够表达“有没有锁”、“重入了几次”、“还剩几个许可”这些信息而且 JVM 对 int 的 CAS 支持最好。这个字段用volatile修饰保证它在多线程之间的可见性。你看 ReentrantLock 里线程拿到锁就把 state 从 0 改成 1重入一次加 1释放一次减 1减到 0 才算真正释放。Semaphore 里创建时就设好初始许可数比如new Semaphore(5)就是 state 初始化为 5每次 acquire 成功就减 1release 就加 1。注意state 不是 private final因为不同子类对它的初始值要求不一样。但所有修改都必须走getState()、setState()、compareAndSetState()这三个方法不能直接改字段。真正操作 state 的时候靠的是unsafe.compareAndSwapInt这套 CAS 机制。CAS 的全称是 Compare And Swap意思就是“先比一比再交换”。线程尝试把 state 从 0 改成 1 的时候底层会检查当前值到底是不是 0是 0 才更新成 1不是就失败重试。这个操作是硬件级别的原子指令所以不需要加锁效率很高。2.2 CLH 队列变体线程排队的底层结构当 CAS 失败也就是线程抢不到锁的时候该去哪儿AQS 内部维护了一个 FIFO 的双向链表这个链表是 CLH 锁队列的变体。CLH 原本是一种基于自旋的排队锁AQS 把它改造了一下节点之间不仅用 next 连起来还加了 prev 反向指针节点里不仅有线程引用还有一个 waitStatus 字段记录节点状态。每个节点对应一个线程。新来的线程抢锁失败会被封装成一个 Node 节点通过 CAS 追加到链表尾部然后进入等待状态。链表头节点是当前持有锁的线程或者一个虚拟头节点持有锁的线程释放锁之后会去唤醒头节点后面的第一个真正等待的线程。这个设计和去餐厅排队拿号一模一样你来晚了取一个号在后面等着前一个人处理完了服务员喊下一个号。AQS 的队列就是那个“等候区”。它的先进先出特性保证了公平性——至少在线程排队这个环节绝对公平。但注意非公平锁所谓的“不公平”发生在排队之前详见 ReentrantLock 的解析。Node 节点内部的 waitStatus 有几种取值CANCELLED 表示线程取消等待了SIGNAL 表示这个节点的后继节点需要被唤醒CONDITION 表示节点在条件队列里PROPAGATE 表示共享模式下需要向后传播唤醒。这些状态位初看繁琐理解之后会发现它们就是线程状态机的一部分。2.3 模板方法设计子类决定“能不能拿到锁”AQS 的骨架方法全是 final 的比如acquire()、acquireShared()、release()、releaseShared()。这些方法定义了一套完整的获取锁、入队等待、中断处理、释放锁、唤醒后继节点的算法流程。但其中有几个关键步骤被刻意空出来交给子类重写这就是模板方法模式。这几个钩子方法就是tryAcquire(int arg)、tryRelease(int arg)、tryAcquireShared(int arg)、tryReleaseShared(int arg)。它们的返回值含义很直接方法返回值含义tryAcquiretrue 表示拿到锁false 表示失败进入排队tryReleasetrue 表示完全释放false 表示还有重入未释放完tryAcquireShared负数表示失败0 表示成功但无剩余资源正数表示成功且有剩余tryReleaseSharedtrue 表示可能唤醒等待线程以acquire(int arg)为例它的模板流程是先调用tryAcquire(arg)成功就直接返回失败就构造一个独占节点加入队列尾部然后在队列里自旋或挂起直到被前驱节点唤醒如果线程在等待过程中被中断还会响应中断。整个流程对子类完全透明子类只需要回答一个问题“我现在这个状态能不能获取成功”这就是为什么自定义一个基于 AQS 的锁特别简单你几乎只写 tryAcquire 和 tryRelease 两个方法就行其他排队、唤醒、中断响应这些脏活累活AQS 全包了。2.4 独占与共享两组不同的获取/释放流程AQS 把同步器分成两种模式。独占模式的意思是资源同一时刻只能被一个线程持有典型代表是 ReentrantLock。共享模式的意思是资源可以被多个线程同时持有典型代表是 Semaphore 和 CountDownLatch。这两种模式对应了两套流程。独占模式下获取锁走acquire()内部用addWaiter(Node.EXCLUSIVE)创建独占节点释放锁走release()唤醒后继节点用unparkSuccessor()。共享模式下获取走acquireShared()内部用addWaiter(Node.SHARED)释放走releaseShared()不同的是共享模式在唤醒后继节点时会通过setHeadAndPropagate()继续往后传播唤醒直到没有更多资源可分配。举个例子你就明白了。Semaphore 初始 state 是 55 个线程同时进来各拿走 1state 变成 0。第 6 个线程进不来只能在队列里等着。前面某个线程 release 之后 state 变成 1唤醒队首线程这个线程拿走了 state 变成 0。此时如果还有线程在排队理论上继续往后唤醒也拿不到资源所以传播逻辑会判断剩余资源有余量才继续唤醒否则就停下来等下一次 release。3. 源码级实操从零手写基于 AQS 的锁3.1 十分钟手写一个可重入的排它锁光看原理容易飘自己动手写一个基于 AQS 的锁是最快的理解方式。咱们写一个最简单的互斥锁不支持重入但功能完整能 lock 和 unlockpublic class SimpleLock extends AbstractQueuedSynchronizer { Override protected boolean tryAcquire(int arg) { // CAS 把 state 从 0 改成 1成功就表示拿到了锁 if (compareAndSetState(0, 1)) { // 记录当前持有锁的线程这个字段在 AbstractOwnableSynchronizer 里 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } Override protected boolean tryRelease(int arg) { // 如果 state 已经是 0说明锁没被持有不能释放 if (getState() 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } public void lock() { acquire(1); } public void unlock() { release(1); } }这段代码不到 20 行却拥有了完整的互斥语义。lock()调用acquire(1)AQS 内部先调tryAcquire(1)成功就返回失败就自动把当前线程加入等待队列并挂起。unlock()调release(1)内部调tryRelease(1)然后自动唤醒队列头部的下一个线程。你想让它支持可重入只要把 tryAcquire 改成如果当前线程已经是持有者state 直接加 1tryRelease 改成 state 减 1减到 0 才返回 true。这样 ReentrantLock 的核心逻辑就出来了。实际上ReentrantLock 的 Sync 内部类就是这么干的只不过更严谨地处理了溢出的情况。实操心得自己写完后用一个多线程循环 的测试去验证你会发现无论怎么跑结果都不会出错。这种“模板方法 CAS”的优雅只有亲手实现一遍才能体会。3.2 ReentrantLock 加锁流程逐行拆解现在以 ReentrantLock 非公平锁为例走一遍完整的加锁流程。先看非公平锁的lock()方法final void lock() { // 一进来先直接抢一次锁这就是“非公平”的体现 if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); } else { acquire(1); } }发现区别了吗非公平锁在进入 AQS 模板流程之前会先自己 CAS 一次尝试插队。如果正好赶上锁被释放这个新来的线程能直接抢在队列里等了很久的线程前面拿到锁。这很像餐厅里来了个“熟人”不用排队直接进去了。公平锁则没有这一步它无条件进入acquire(1)而且在 tryAcquire 里会先问hasQueuedPredecessors()队列里只要有线程在等自己就去排队。为什么非公平锁性能更好因为每次唤醒一个线程都需要用户态到内核态的切换park/unpark开销不小。非公平锁允许新线程在锁刚释放的瞬间直接通过 CAS 拿到省掉了一次线程唤醒和挂起的上下文切换。所以实际场景里没有特殊公平性需求默认都选非公平锁。进入acquire(1)之后会执行到tryAcquire(1)非公平锁对应的实现是nonfairTryAcquire。它会先看 state如果是 0说明锁没被占尝试 CAS 抢如果不是 0但当前线程已经是持有锁的线程说明是重入state 加上 acquires 然后返回 true。如果两者都不满足返回 false进入 addWaiter。addWaiter(Node.EXCLUSIVE)会创建一个独占节点CAS 方式追加到队列尾部然后调用acquireQueued(node, 1)。在acquireQueued里线程检查前驱节点是不是 head如果是再试一次 tryAcquire成功就把自己设为新的 head之前的头节点出队。如果失败或前驱不是 head就通过shouldParkAfterFailedAcquire确认前驱节点状态然后LockSupport.park(this)挂起。等持有锁的线程释放时会调用unparkSuccessor把队首等待节点的线程唤醒被唤醒的线程继续循环再次尝试获取锁。这个过程从头到尾没有任何一行代码用 synchronized但多线程竞争下的安全性和顺序性都保证了。核心就是三件事CAS 改状态、链表排队、park/unpark 挂起唤醒。3.3 Condition 条件队列await 和 signal 的秘密AQS 里还有一个容易被忽视但很重要的东西ConditionObject。它做的事情和 Object 的 wait/notify 类似但更灵活——一个锁可以创建多个 Condition比如ReentrantLock的newCondition()。class BoundedBuffer { final Lock lock new ReentrantLock(); final Condition notFull lock.newCondition(); final Condition notEmpty lock.newCondition(); // 入队时用 notFull.await() 等不满出队时用 notEmpty.await() 等不空 }AQS 内部给每个 ConditionObject 维护了一个单向链表节点就是 Node 的 CONDITION 状态节点。线程调用await()时会被封装成 CONDITION 节点放入条件队列然后释放当前持有的锁把 state 减到 0 并唤醒下一个排队线程最后挂起。线程调用signal()时会把条件队列头部的节点转移到 AQS 的同步队列里让它重新参与锁竞争。这个“先释放锁、再挂起”的设计很关键。如果 await 不释放锁其他线程永远拿不到锁也就无法调用 signal 来唤醒它死锁就出现了。条件的本质是“在满足某个业务条件之前先交出手里的锁等条件满足后再回来继续干活”。容易踩的坑在 await() 之后线程被唤醒不等于条件已经满足通常要放在 while 循环里重新检查条件这就是著名的“条件等待必须循环判断”原则。直接写 if 判断条件可能出现伪唤醒导致逻辑出错。4. 常见问题与排查技巧实录4.1 三个最常见的 AQS 理解误区第一个误区以为 AQS 本身就是一个锁。实际上 AQS 只是框架ReentrantLock、Semaphore 这些工具各自定义了 state 的语义和 tryXxx 方法才形成了具体的锁或同步器。AQS 单独存在时什么都不做它只是“发动机”装在不同的车上才表现不同。第二个误区以为 AQS 的队列在一直自旋。它确实是 CLH 锁队列的变体但绝大多数线程在抢锁失败后会被 park 挂起而不是在那空转消耗 CPU。只有前驱节点是 head 的线程在唤醒后会自旋重试一次随后依然可能被挂起。所以它叫“CLH 变体”不是纯自旋锁。第三个误区把 state 简单等同于锁重入次数。state 的含义完全由具体子类决定。在 CountDownLatch 里state 是构造时传入的计数每次countDown()减 1直到变成 0 才放行。在读写锁里state 的高 16 位表示读锁数量低 16 位表示写锁数量。理解 AQS核心是理解“state 只是一个承载同步状态信息的整数载体”。4.2 线上线程卡死如何定位到具体锁AQS 和 synchronized 不同排队等待的线程不会被标记为 BLOCKED 状态而是 WAITING 或 TIMED_WAITING。很多人在排查线上问题时会疑惑线程明明没干活为什么显示 WAITING其实它很可能在 AQS 队列里排队。我用 jstack 抓线程栈时经常看到这样的输出pool-1-thread-2 #12 prio5 os_prio31 cpu0.24ms elapsed100.58s tid0x00007f8e7d0e8000 nid0x6703 waiting on condition [0x000070000e2b1000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:1058)WAITING (parking)这个状态就是典型的 AQS 等待。如果想进一步知道它等的是哪把锁往下翻栈帧找到java.util.concurrent.locks.ReentrantLock$NonfairSync.lock或者Semaphore.acquire这类方法就能初步定位到对象。如果线程同时握着多把锁还互相等待jstack 会明确输出Found one Java-level deadlock并列出死锁的线程和锁的持有关系。我实际排查过一个线程池任务全部卡死的问题最终发现是某个任务里用CountDownLatch.await()等一个永远不会释放的信号量导致线程池工作线程全部陷入 AQS 队列等待。所以排查等待问题时不能只盯 BLOCKED 的线程WAITING (parking)的线程同样可能是问题源头。4.3 面试高频问题与回答思路AQS 相关面试题翻来覆去就那几个核心考点我整理了一张速查表问题回答要点AQS 是什么一个用于构建锁和同步器的抽象框架维护了 volatile int state 和一个 FIFO 等待队列ReentrantLock 和 synchronized 区别ReentrantLock 基于 AQS支持公平/非公平、可中断、超时、多个 Conditionsynchronized 是 JVM 内置锁公平锁和非公平锁实现差异非公平锁进入时先直接 CAS 抢一次公平锁在 tryAcquire 里检查 hasQueuedPredecessorsacquire 失败后线程做什么封装成 Node 加入队尾通过 LockSupport.park 挂起前驱节点唤醒后才继续竞争唤醒的是哪个线程头节点的后继节点也就是等待队列里等待时间最长的线程AQS 怎么支持共享模式的传播唤醒setHeadAndPropagate 在释放资源后检查剩余资源向后面节点传递唤醒信号Condition 与 Object.wait/notify 的区别Condition 需要和 Lock 绑定支持多条件队列支持超时和中断更灵活自旋锁和 AQS 有什么关系AQS 的 CLH 队列是自旋锁的变体但实际主要用 park 挂起不是全程自旋面试时可以从一个点切入比如“为什么用 CAS 而不是 synchronized”然后自然引申到内存模型、锁竞争、阻塞唤醒开销。AQS 是入口但面的是你对并发底层的整体理解。平时多看看源码多写几个自定义同步器答题时底气完全不一样。我个人看完源码之后最大的收获反而不是“我会写锁了”而是理解了为什么这么多并发工具能构建在同一套机制上以及为什么优秀的框架总是把通用的复杂逻辑封装好把变化点留给开发者。AQS 用它那不到千行的代码撑起了 Java 并发包的大半个江山。你现在再回头去看 ReentrantLock 或 Semaphore 的源码应该会有一种“原来如此”的感觉——希望这篇文章也能帮你打通这最后一层窗户纸。