type
Post
status
Published
date
Oct 12, 2025
slug
java3
summary
tags
技术探索
category
icon
password
Java 并发里有一类工具非常常用:
ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLockThreadPoolExecutor中的一些并发控制逻辑
这些工具看起来用途不同,但它们背后有一个非常重要的基础框架:AQS。
AQS,全称是
AbstractQueuedSynchronizer,中文通常叫“抽象队列同步器”。它不是一把具体的锁,而是一套用来构建锁和同步器的基础框架。
理解 AQS 之后,再看
ReentrantLock、Semaphore、CountDownLatch,就会发现它们并不是孤立的工具类,而是同一套底层模型的不同应用。一、AQS 到底解决了什么问题?
先不看源码,先想一个问题:如果自己实现一把锁,需要解决什么?
至少要解决这些问题:
第一,如何表示锁有没有被占用?第二,多个线程同时抢锁时,如何保证只有一个线程成功?第三,抢锁失败的线程怎么办?第四,线程阻塞后放在哪里排队?第五,锁释放后,如何唤醒后面的线程?第六,如何支持可重入、公平锁、非公平锁、中断、超时等待?
AQS 就是为了解决这些通用问题而设计的。
它把同步器里最复杂、最通用的部分抽象了出来:
用state表示同步状态。用 CAS 保证状态修改的原子性。用 FIFO 队列管理抢锁失败的线程。用LockSupport.park()挂起线程。用LockSupport.unpark()唤醒线程。通过模板方法让子类定义具体的获取和释放逻辑。
所以,AQS 的核心可以浓缩成一句话:
AQS 是一个基于state + CAS + FIFO 等待队列 + park/unpark实现的同步器框架。
二、AQS 的核心结构
先看 AQS 最核心的几个字段。
下面不是完整源码,而是保留关键结构后的精简版,方便理解。
这三个字段非常关键:
state负责表达同步状态。head和tail负责维护等待队列。volatile保证不同线程之间的可见性。修改state、head、tail时,关键位置会使用 CAS 保证并发安全。
这里要特别注意一点:AQS 自己并不知道
state 具体代表什么。state 在不同同步器里的含义完全不同。在ReentrantLock中,state是锁重入次数。在Semaphore中,state是许可证数量。在CountDownLatch中,state是倒计时数量。在ReentrantReadWriteLock中,state会同时表达读锁和写锁状态。
这就是 AQS 设计得比较抽象的地方:它只提供通用机制,不规定具体业务语义。
三、state 的读取和 CAS 修改
AQS 对
state 的操作,核心是下面几个方法。这段源码要抓住三个点:
getState()依赖volatile保证可见性。compareAndSetState()依赖 CAS 保证原子性。多个线程竞争锁时,最终只有一个线程能 CAS 成功。
以
ReentrantLock 为例,多个线程同时看到 state = 0,都想把它改成 1。但是 CAS 会保证只有一个线程成功。
其他线程 CAS 失败后,就不能继续执行临界区,而是要进入 AQS 等待队列。
四、AQS 的等待队列 Node
抢锁失败的线程,会被包装成一个
Node,放到 AQS 的同步队列中。可以把这个队列理解成一个 FIFO 双向链表。
简化后的
Node 结构如下:这个结构很重要。
AQS 的同步队列可以简单理解成这样:
head -> node1 -> node2 -> node3 -> tail
每个
node 里都包装了一个等待线程。当线程抢锁失败后,会进入队尾。
当前持有锁的线程释放锁后,会从队头附近找到下一个需要唤醒的线程。
注意,AQS 的同步队列不是普通的
BlockingQueue,它是 AQS 自己维护的 CLH 变体队列。它的核心特点是:
先进先出。每个节点知道自己的前驱和后继。线程是否应该阻塞,通常要看前驱节点状态。唤醒时通常从头节点的后继开始找。
五、AQS 的模板方法设计
AQS 的设计非常典型:父类负责排队、阻塞、唤醒,子类负责判断能不能获取资源。
AQS 中有几个方法默认是抛异常的,交给子类去实现。
这种设计非常巧妙。
AQS 说:
我不关心你这个同步器是锁、信号量,还是倒计时器。你只要告诉我“能不能获取成功”和“能不能释放成功”。至于失败后怎么排队、怎么阻塞、怎么唤醒,我来处理。
这就是为什么
ReentrantLock、Semaphore、CountDownLatch 都可以基于 AQS 实现。六、独占模式:ReentrantLock 的核心流程
ReentrantLock 是 AQS 独占模式的典型代表。独占模式的意思是:
同一时刻,只能有一个线程成功获取同步状态。
先看
ReentrantLock.lock() 的调用链。ReentrantLock 内部有公平锁和非公平锁两种实现。默认是非公平锁。
非公平锁和公平锁的区别,主要体现在获取锁时是否允许插队。
七、非公平锁源码理解
非公平锁的核心逻辑可以简化成这样:
这段源码有一个关键点:
非公平锁一上来就 CAS 抢锁。即使队列里已经有线程在等待,新来的线程也可能插队成功。所以非公平锁吞吐量通常更高,但可能导致等待线程饥饿。
再看
nonfairTryAcquire() 的核心逻辑。这段代码解释了
ReentrantLock 的两个核心能力:state = 0时,表示锁空闲。CAS 成功后,当前线程持有锁。如果当前线程已经持有锁,再次加锁就是重入。重入时state会递增。释放锁时,state会递减。只有state减到 0,锁才算真正释放。
所以,可重入锁的本质就是:
同一个线程重复加锁时,不会被自己阻塞,而是增加重入计数。
八、公平锁源码理解
公平锁和非公平锁最大的区别是:公平锁会先检查队列中是否有线程在排队。
简化后的公平锁获取逻辑如下:
公平锁多了一个关键判断:
这个判断的含义是:
如果队列中已经有线程排在当前线程前面,当前线程不能插队。当前线程必须老老实实进入队列等待。
所以公平锁和非公平锁可以这样理解:
非公平锁:先抢一下,抢不到再排队。公平锁:先看有没有人排队,没人排队才抢。
非公平锁吞吐通常更高,因为它减少了线程切换和唤醒成本。
公平锁更符合先来先服务,但性能通常会低一些。
九、AQS acquire 流程
当线程直接获取锁失败后,会进入 AQS 的
acquire() 流程。简化版源码如下:
这段代码很短,但包含了 AQS 独占模式的核心流程:
第一步,调用子类实现的tryAcquire()尝试获取锁。第二步,如果获取失败,就调用addWaiter()把当前线程加入等待队列。第三步,调用acquireQueued(),让当前线程在队列中等待获取锁。第四步,如果等待过程中发生过中断,最后调用selfInterrupt()补一次中断标记。
这里一定要注意:
AQS 自己不判断锁能不能获取。
它只调用子类的
tryAcquire()。真正判断逻辑在
ReentrantLock.Sync 里。selfInterrupt() 也有一个细节:普通lock()在等待过程中即使被中断,也不会立刻抛出异常退出。但 AQS 会记录等待过程中是否发生过中断。等线程最终拿到锁后,再通过selfInterrupt()把中断标记补回来。
十、addWaiter:把线程加入等待队列
当线程获取锁失败后,会被包装成 Node 加入队列。
这段代码做了两件事:
如果队列已经存在,就先尝试快速追加到队尾。如果失败,就进入enq()自旋入队。
为什么需要 CAS?
因为多个线程可能同时抢锁失败,然后同时要加入队尾。
如果不用 CAS,队列结构可能被并发写坏。
十一、enq:自旋入队
enq() 会保证节点一定能入队成功。这段代码里最关键的是:
AQS 队列是懒初始化的。第一次有线程入队时,会先创建一个空的头节点。后续节点通过 CAS 追加到队尾。如果 CAS 失败,说明有其他线程抢先修改了tail,当前线程继续自旋重试。
这里的自旋不是一直抢锁,而是为了保证入队操作完成。
入队成功之后,线程后续就可能被挂起,不会一直空转消耗 CPU。
十二、acquireQueued:入队后的等待逻辑
线程入队之后,会在
acquireQueued() 里继续尝试获取锁。这段源码非常重要。
AQS 队列中的线程不是被唤醒后就一定能拿到锁。
它必须满足两个条件:
当前节点的前驱节点是head。调用tryAcquire()获取锁成功。
为什么必须前驱是
head?因为 AQS 要尽量保证队列顺序。
如果当前节点不是队列里的第一个等待节点,它不应该越过前面的节点去抢锁。
这里也能看出公平性和队列顺序的关系。
十三、shouldParkAfterFailedAcquire:什么时候可以阻塞?
线程入队后,不会立刻无脑
park()。它会先检查前驱节点状态。
这段逻辑解决了一个关键问题:
当前线程阻塞前,要确保前驱节点将来会唤醒自己。
如果前驱节点状态是
SIGNAL,说明它释放锁时会唤醒后继节点。这时当前线程才能安全地阻塞。
如果不做这个判断,可能出现线程睡过去之后没人叫醒它的问题。
所以 AQS 的阻塞不是随便阻塞,而是有严格前置条件的。
十四、parkAndCheckInterrupt:真正挂起线程
真正阻塞线程的是
LockSupport.park()。这段代码要注意几个点:
park()可能因为unpark()返回。park()也可能因为中断返回。park()还允许虚假返回。所以线程从park()返回后,不能默认认为自己已经拿到锁。正确做法是回到循环里重新检查条件。
这也是为什么
acquireQueued() 外层是一个无限循环。线程每次被唤醒后,都要重新判断:
我是不是队列中第一个等待节点?我现在能不能获取锁?
如果不能,继续等待。
十五、release:释放锁并唤醒后继节点
加锁看完,再看释放锁。
AQS 独占模式释放逻辑如下:
这里同样是模板方法:
AQS 调用子类的tryRelease()判断是否释放成功。如果释放成功,再由 AQS 唤醒后继节点。
对
ReentrantLock 来说,tryRelease() 要处理可重入。这段代码说明:
重入几次,就要释放几次。只有state减到 0,锁才真正释放。非持有锁线程调用unlock()会抛异常。锁完全释放后,AQS 才会唤醒等待队列中的后继线程。
这也是为什么
lock() 必须配合 try-finally 使用。十六、unparkSuccessor:唤醒后继节点
锁释放后,AQS 会唤醒后继节点。
这段逻辑有两个细节。
第一,为什么不直接唤醒
node.next?因为并发取消、超时、中断等情况可能导致
next 不可靠。所以如果
next 不合适,AQS 会从 tail 往前找有效节点。第二,为什么只是
unpark(),不是直接让线程获得锁?因为被唤醒的线程只是恢复运行,它还要回到
acquireQueued() 的循环中重新尝试获取锁。所以:
唤醒不等于获取锁成功。unpark()只是让线程有机会继续竞争。真正能不能拿到锁,还要看tryAcquire()是否成功。
十七、独占模式完整流程总结
以
ReentrantLock 为例,完整加锁流程可以串起来:线程调用lock()。非公平锁先 CAS 抢一次state。抢成功,设置当前线程为锁持有者。抢失败,进入 AQS 的acquire()。acquire()再调用tryAcquire()尝试一次。仍然失败,当前线程被包装成Node加入同步队列。入队后,如果前驱是head,就继续尝试获取锁。获取失败后,确认前驱会唤醒自己,再调用park()阻塞。持锁线程调用unlock()。tryRelease()把state减一。如果state变成 0,说明锁完全释放。AQS 调用unparkSuccessor()唤醒后继节点。被唤醒线程继续循环,尝试获取锁。
这就是 AQS 独占模式的核心。
可以再浓缩一下:
获取失败就入队。入队后判断是否轮到自己。没轮到就安全阻塞。释放锁时唤醒后继。被唤醒后重新竞争锁。
十八、共享模式:Semaphore 和 CountDownLatch
AQS 不只支持独占模式,还支持共享模式。
共享模式的特点是:
一次释放可能允许多个线程继续执行。
典型代表是:
SemaphoreCountDownLatchReentrantReadWriteLock的读锁
独占模式和共享模式的差异可以这样理解:
独占模式:同一时刻通常只有一个线程能获取成功,比如ReentrantLock。共享模式:同一时刻可以有多个线程获取成功,比如Semaphore有多个许可证时,多个线程都能通过。
十九、Semaphore 的 AQS 实现思路
Semaphore 用 state 表示许可证数量。比如:
可以理解为:
AQS 的state = 3。每次acquire()获取一个许可证,state - 1。每次release()释放一个许可证,state + 1。如果state不够,线程进入等待队列。
简化后的非公平获取许可证逻辑如下:
返回值含义很重要:
返回值小于 0:获取失败,需要排队。返回值大于等于 0:获取成功,可以继续执行。
释放许可证逻辑如下:
这就是
Semaphore 的核心:获取许可证,本质是 CAS 减少state。释放许可证,本质是 CAS 增加state。许可证不足时,线程进入 AQS 队列等待。
Semaphore 适合控制并发数量。比如:
限制同时访问某个接口的线程数。限制同时处理文件上传的任务数。限制同时访问某类下游资源的并发数。模拟连接池中的许可证控制。
二十、CountDownLatch 的 AQS 实现思路
CountDownLatch 用 state 表示剩余计数。比如:
可以理解为:
AQS 的state = 3。每次调用countDown(),state - 1。调用await()的线程会判断state是否为 0。如果state != 0,线程进入等待。当state == 0,所有等待线程可以继续执行。
简化后的
await() 获取逻辑如下:再看
countDown() 的释放逻辑:这段代码解释了
CountDownLatch 为什么是一次性的。因为
state 减到 0 之后,不会再恢复。所以:
CountDownLatch不能重置。它适合一次性等待多个任务完成。如果需要重复使用屏障,应使用CyclicBarrier或其他工具。
二十一、共享模式 acquireShared 流程
AQS 的共享模式入口大致如下:
这段逻辑和独占模式很像:
先调用子类的tryAcquireShared()。如果返回值小于 0,说明获取失败。获取失败后进入共享模式的排队等待逻辑。
共享模式释放入口如下:
共享模式释放成功后,会调用
doReleaseShared()。共享模式和独占模式最大的区别是:
独占模式通常一次只唤醒一个后继节点。共享模式可能需要继续传播唤醒,让多个等待线程都有机会继续执行。
比如
CountDownLatch 中,计数归零后,所有 await() 的线程都应该被唤醒继续执行。二十二、Condition 和 AQS 的关系
ReentrantLock 可以创建多个 Condition。例如:
这和
wait/notify 很像,但更灵活。wait/notify 只有一个对象等待队列,而 Condition 可以有多个条件队列。典型阻塞队列中可以这样设计:
队列为空时,消费者等待notEmpty。队列满时,生产者等待notFull。入队后唤醒notEmpty。出队后唤醒notFull。
简化示例:
这段代码比
wait/notify 更清晰,因为生产者和消费者可以等待在不同的条件队列中。Condition 的优势是:可以有多个条件队列。可以精准唤醒等待某个条件的线程。比wait/notify更适合复杂的生产者消费者场景。底层仍然依赖 AQS 的队列机制。
二十三、ConditionObject 的核心源码理解
Condition 在 AQS 中对应的是 ConditionObject。每个
ConditionObject 内部都有一条条件队列。注意,AQS 里有两类队列:
同步队列:抢锁失败的线程在这里排队。条件队列:调用Condition.await()的线程在这里等待条件。
这两个队列不是一回事。
调用
await() 时,线程会从持锁状态转移到条件队列中。简化流程如下:
这段源码可以拆成几个关键步骤:
当前线程必须先持有锁。调用await()后,线程进入 Condition 条件队列。进入条件队列后,会释放当前持有的锁。然后线程被park()挂起。其他线程调用signal()后,该节点会从条件队列转移到 AQS 同步队列。被唤醒后,线程不是立刻继续执行,而是要重新竞争锁。重新拿到锁后,await()才能返回。
这和
Object.wait() 很像:wait()会释放synchronized锁。Condition.await()会释放ReentrantLock。被唤醒后,都需要重新获取锁,才能继续执行。
二十四、signal 的核心流程
再看
signal() 的简化逻辑。doSignal() 的核心思想是:这段逻辑最重要的是:
signal()不是直接让等待线程继续执行业务代码。它只是把节点从 Condition 条件队列转移到 AQS 同步队列。被唤醒线程还要重新竞争锁。只有重新获取锁成功后,await()才会返回。
这点很容易被忽略。
很多人以为
signal() 之后,对方线程马上执行。其实不是。
二十五、AQS 为什么要分同步队列和条件队列?
同步队列解决的是:
线程抢锁失败后,在哪里排队?
条件队列解决的是:
线程已经拿到锁,但发现业务条件不满足,应该在哪里等待?
举个例子:阻塞队列为空时,消费者线程进入
take()。它先拿到锁,但发现队列为空。
这时它不是抢锁失败,而是业务条件不满足。
所以它应该进入
notEmpty 条件队列等待。当生产者放入元素后,调用
notEmpty.signal(),消费者才从条件队列转移到同步队列,重新竞争锁。所以完整路径是:
获取锁成功。发现条件不满足。调用await(),释放锁,进入条件队列。其他线程改变条件。调用signal()。节点从条件队列转移到同步队列。线程重新竞争锁。获取锁成功后,从await()返回,继续执行业务。
这个设计让
ReentrantLock + Condition 可以精细控制多个条件等待队列。二十六、AQS 中断和超时能力
AQS 不只支持普通加锁,还支持中断和超时。
比如
ReentrantLock 中:表示等待锁的过程中可以响应中断。
还有:
表示最多等待一段时间,超时后返回失败。
这类能力底层也依赖 AQS 的不同 acquire 方法。
简化理解:
普通lock():拿不到锁就一直等,不直接响应中断退出。lockInterruptibly():等待过程中如果被中断,可以抛出InterruptedException。tryLock(timeout):等待指定时间,超时后放弃。tryLock():立即尝试一次,成功返回 true,失败返回 false。
这些能力是
synchronized 不直接具备的。所以在需要超时、可中断、降级的场景下,
ReentrantLock 更灵活。二十七、AQS 和 synchronized 的区别
AQS 不是为了替代 synchronized,它们位于不同层面。synchronized 是 JVM 内置锁,基于对象监视器。AQS 是 Java 层面的同步器框架,基于
state、CAS、队列和 LockSupport。可以这样理解:
synchronized是语言级内置能力。AQS 是 JUC 并发包里的基础框架。synchronized使用简单,自动释放锁。AQS 支持更丰富的同步器能力,比如公平锁、非公平锁、可中断、超时、共享模式、多个条件队列。
很多高级同步工具之所以强大,就是因为 AQS 把阻塞、排队、唤醒这些复杂逻辑封装好了。
二十八、从线程栈看 AQS
线上如果大量线程卡在
ReentrantLock,线程栈里经常会看到类似结构:这说明线程大概率在等待 AQS 队列中的锁资源。
如果看到很多线程都在
park(),不要简单理解为线程“死了”。它们通常是在等待某个同步条件满足。
排查时要重点看:
哪个线程持有锁?持锁线程在做什么?是否在锁内进行了慢 SQL、RPC、文件 IO?是否忘记释放锁?是否产生了死锁或长时间阻塞?线程池是否因为等待锁被打满?
AQS 的知识不仅能帮助理解源码,也能帮助定位线上线程阻塞问题。
二十九、AQS 常见误区
1. AQS 是一把锁吗?
不是。
AQS 是同步器框架。
ReentrantLock 才是具体的锁。AQS 提供的是:
状态管理。CAS 修改。等待队列。阻塞和唤醒。独占模式和共享模式。
2. state 的含义是固定的吗?
不是。
state 只是一个 int 状态值,具体含义由子类决定。ReentrantLock中表示重入次数。Semaphore中表示许可证数量。CountDownLatch中表示剩余计数。ReentrantReadWriteLock中会拆分高低位表示读写状态。
3. 被 unpark 后是不是马上拿到锁?
不是。
unpark() 只是让线程从阻塞状态恢复运行。恢复后还要重新进入循环,尝试获取锁。
所以:
唤醒不等于获取锁成功。被唤醒后仍然要竞争。条件不满足时可能继续阻塞。
4. 非公平锁是不是一定不按顺序?
非公平锁不是完全乱序。
它只是允许新来的线程先抢一次。
如果抢不到,还是要进入 AQS 队列排队。
所以非公平锁可以理解为:
允许插队,但不是完全没有队列规则。
5. CountDownLatch 为什么不能复用?
因为它的
state 从初始值递减到 0 后,不会再恢复。如果需要重复使用同步屏障,应该考虑
CyclicBarrier。三十、总结
AQS 是 Java 并发工具的核心底座之一。
它的核心模型可以总结为:
一个volatile int state表示同步状态。CAS 用来安全修改state。抢资源失败的线程会进入 FIFO 同步队列。队列节点中保存等待线程和等待状态。线程需要阻塞时,通过LockSupport.park()挂起。资源释放后,通过LockSupport.unpark()唤醒后继线程。子类通过tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared定义具体语义。
独占模式的代表是
ReentrantLock:state = 0表示锁空闲。state > 0表示锁被持有。同一线程重复加锁会让state递增。解锁时state递减。只有state回到 0,锁才真正释放。
共享模式的代表是
Semaphore 和 CountDownLatch:Semaphore用state表示许可证数量。CountDownLatch用state表示剩余计数。共享模式释放成功后,可能唤醒多个等待线程。
Condition 的核心是条件队列:
await()会释放锁,并让线程进入条件队列。signal()会把节点从条件队列转移到 AQS 同步队列。被唤醒线程需要重新竞争锁,拿到锁后才能继续执行。
理解 AQS 之后,再看 JUC 并发工具,会清晰很多。
你会发现它们本质上都在回答同一组问题:
同步状态如何表示?状态修改如何保证原子性?获取失败的线程放在哪里?线程什么时候应该阻塞?资源释放后应该唤醒谁?独占和共享场景如何统一抽象?
这就是 AQS 的价值。
分享
