Lazy loaded image
Java丨AQS 原理:从 state、CAS 到同步队列一次讲清楚
Words 12195Read Time 31 min
2025-10-12
type
Post
status
Published
date
Oct 12, 2025
slug
java3
summary
tags
技术探索
category
icon
password
Java 并发里有一类工具非常常用:
ReentrantLock
Semaphore
CountDownLatch
ReentrantReadWriteLock
ThreadPoolExecutor 中的一些并发控制逻辑
这些工具看起来用途不同,但它们背后有一个非常重要的基础框架:AQS
AQS,全称是 AbstractQueuedSynchronizer,中文通常叫“抽象队列同步器”。
它不是一把具体的锁,而是一套用来构建锁和同步器的基础框架。
理解 AQS 之后,再看 ReentrantLockSemaphoreCountDownLatch,就会发现它们并不是孤立的工具类,而是同一套底层模型的不同应用。

一、AQS 到底解决了什么问题?

先不看源码,先想一个问题:如果自己实现一把锁,需要解决什么?
至少要解决这些问题:
第一,如何表示锁有没有被占用?
第二,多个线程同时抢锁时,如何保证只有一个线程成功?
第三,抢锁失败的线程怎么办?
第四,线程阻塞后放在哪里排队?
第五,锁释放后,如何唤醒后面的线程?
第六,如何支持可重入、公平锁、非公平锁、中断、超时等待?
AQS 就是为了解决这些通用问题而设计的。
它把同步器里最复杂、最通用的部分抽象了出来:
state 表示同步状态。
用 CAS 保证状态修改的原子性。
用 FIFO 队列管理抢锁失败的线程。
LockSupport.park() 挂起线程。
LockSupport.unpark() 唤醒线程。
通过模板方法让子类定义具体的获取和释放逻辑。
所以,AQS 的核心可以浓缩成一句话:
AQS 是一个基于 state + CAS + FIFO 等待队列 + park/unpark 实现的同步器框架。

二、AQS 的核心结构

先看 AQS 最核心的几个字段。
下面不是完整源码,而是保留关键结构后的精简版,方便理解。
这三个字段非常关键:
state 负责表达同步状态。
headtail 负责维护等待队列。
volatile 保证不同线程之间的可见性。
修改 stateheadtail 时,关键位置会使用 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 说:
我不关心你这个同步器是锁、信号量,还是倒计时器。
你只要告诉我“能不能获取成功”和“能不能释放成功”。
至于失败后怎么排队、怎么阻塞、怎么唤醒,我来处理。
这就是为什么 ReentrantLockSemaphoreCountDownLatch 都可以基于 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 不只支持独占模式,还支持共享模式。
共享模式的特点是:
一次释放可能允许多个线程继续执行。
典型代表是:
Semaphore
CountDownLatch
ReentrantReadWriteLock 的读锁
独占模式和共享模式的差异可以这样理解:
独占模式:同一时刻通常只有一个线程能获取成功,比如 ReentrantLock
共享模式:同一时刻可以有多个线程获取成功,比如 Semaphore 有多个许可证时,多个线程都能通过。

十九、Semaphore 的 AQS 实现思路

Semaphorestate 表示许可证数量。
比如:
可以理解为:
AQS 的 state = 3
每次 acquire() 获取一个许可证,state - 1
每次 release() 释放一个许可证,state + 1
如果 state 不够,线程进入等待队列。
简化后的非公平获取许可证逻辑如下:
返回值含义很重要:
返回值小于 0:获取失败,需要排队。
返回值大于等于 0:获取成功,可以继续执行。
释放许可证逻辑如下:
这就是 Semaphore 的核心:
获取许可证,本质是 CAS 减少 state
释放许可证,本质是 CAS 增加 state
许可证不足时,线程进入 AQS 队列等待。
Semaphore 适合控制并发数量。
比如:
限制同时访问某个接口的线程数。
限制同时处理文件上传的任务数。
限制同时访问某类下游资源的并发数。
模拟连接池中的许可证控制。

二十、CountDownLatch 的 AQS 实现思路

CountDownLatchstate 表示剩余计数。
比如:
可以理解为:
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() 唤醒后继线程。
子类通过 tryAcquiretryReleasetryAcquireSharedtryReleaseShared 定义具体语义。
独占模式的代表是 ReentrantLock
state = 0 表示锁空闲。
state > 0 表示锁被持有。
同一线程重复加锁会让 state 递增。
解锁时 state 递减。
只有 state 回到 0,锁才真正释放。
共享模式的代表是 SemaphoreCountDownLatch
Semaphorestate 表示许可证数量。
CountDownLatchstate 表示剩余计数。
共享模式释放成功后,可能唤醒多个等待线程。
Condition 的核心是条件队列:
await() 会释放锁,并让线程进入条件队列。
signal() 会把节点从条件队列转移到 AQS 同步队列。
被唤醒线程需要重新竞争锁,拿到锁后才能继续执行。
理解 AQS 之后,再看 JUC 并发工具,会清晰很多。
你会发现它们本质上都在回答同一组问题:
同步状态如何表示?
状态修改如何保证原子性?
获取失败的线程放在哪里?
线程什么时候应该阻塞?
资源释放后应该唤醒谁?
独占和共享场景如何统一抽象?
这就是 AQS 的价值。
 
回到首页