欢迎关注我的公众号:观知小阁。包含各种类的文章,内容更丰富,更新及时且不迷路。
如果你还在为Java并发编程中的锁机制感到困惑,如果你在面试中被问到"ReentrantLock底层原理"时支支吾吾,那么这篇文章就是为你准备的。今天,我们来揭开AQS的神秘面纱,理解这个支撑整个Java并发生态的"万能骨架"。
1. 为什么AQS是Java并发的"地基"?
想象一下,你正在使用ReentrantLock加锁、用CountDownLatch等待线程结束、用Semaphore控制并发数——这三个工具功能完全不同,但它们的底层代码都绕不开同一个核心组件:AQS(AbstractQueuedSynchronizer,抽象队列同步器)。
AQS就像盖房子时的钢筋混凝土地基,无论是别墅还是公寓,都离不开这个基础。它定义了锁竞争、线程排队、唤醒的核心规则,让上层工具只需实现个性化逻辑,无需重复造轮子。
看看这些简化源码片段,你会发现惊人的相似性:
// ReentrantLock的lock()方法,底层调用AQS的acquire()
public void lock() {
sync.acquire(1);
}
// CountDownLatch的await()方法,底层调用AQS的acquireShared()
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// Semaphore的acquire()方法,底层调用AQS的acquireShared()
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
无论是独占锁(ReentrantLock)还是共享锁(CountDownLatch、Semaphore),核心操作都委托给了AQS。这就是AQS的核心价值:封装并发编程的共性问题,让上层工具只需关注个性化逻辑。
2. 用"生活场景"看懂AQS的核心原理
光说概念太抽象,我们用"抢厕所"的场景来类比,一下子就能懂:
假设公司只有1个厕所(对应"1把锁"),门口挂着一块牌子(对应AQS的state状态变量):
-
牌子写"0":厕所空闲(锁可用)
-
牌子写"1":厕所占用(锁已被拿)
2.1 线程抢锁:能拿就拿,拿不到排队
当线程A来抢锁时,会先看门口的牌子(state):
-
如果是"0":直接把牌子改成"1",然后进厕所(成功获取锁)
-
如果是"1":没法进,就乖乖站到门口的队伍里(进入AQS的等待队列),并且自己"休眠"(阻塞),等里面的人出来了再喊它
这里有个关键细节:改牌子(state)的操作是"原子的"——就像只有一个人能同时改牌子,不会出现两个线程都看到"0",都改成"1"的情况(这靠CAS操作保证)。
2.2 线程释放锁:改回牌子,喊下一个人
当里面的线程A用完厕所(释放锁),会做两件事:
-
把门口的牌子改回"0"(state重置)
-
从队伍里喊第一个人(唤醒等待队列的头节点线程),让它来抢锁
这样一来,整个抢锁、排队、释放的流程就顺畅了,不会乱套。
3. AQS的两个核心"零件"
想彻底理解AQS,必须记住它的两个核心组成部分,这也是子类实现的关键:

3.1 状态变量(state):锁的"晴雨表"
前面类比的"牌子"就是state,它是一个volatile int变量(保证线程间可见性),不同的子类会赋予它不同的含义:
-
ReentrantLock(可重入锁):state=0表示无锁,state>=1表示有锁(重入一次加1,释放一次减1,减到0才真释放)
-
Semaphore(信号量):state表示"可用的资源数量",线程抢资源时state减1,释放时加1
-
CountDownLatch(倒计时器):state表示"倒计时次数",调用countDown()就减1,减到0时唤醒所有等待线程
AQS提供了3个方法操作state,子类可以直接用:
-
getState():获取当前状态
-
setState(int newState):设置新状态
-
compareAndSetState(int expect, int update):CAS方式改状态(保证原子性,核心中的核心)
3.2 等待队列(CLH队列):线程的"候客厅"
当线程抢不到锁时,会被包装成一个"节点"(Node),加入到一个双向链表里,这就是CLH队列(以发明者Craig、Landin和Hagersten的名字命名)。
这个队列有两个特点:
-
先进先出(FIFO):先排队的线程先被唤醒(公平锁的核心逻辑)
-
带头节点的链表:头节点是"正在用锁的线程",后面的节点是"等待的线程",释放锁时只需要唤醒头节点的下一个节点就行,效率高
每个Node节点包含:
-
等待的线程(thread)
-
前驱节点(prev)、后继节点(next)
-
等待状态(waitStatus):比如"等待中(SIGNAL)"、"取消等待(CANCELLED)"
4. AQS的两种工作模式
AQS支持两种同步模式,适应不同的并发场景:
4.1 独占模式(Exclusive)
特点:同一时间只有一个线程能获取资源,就像卫生间只能一个人用。
应用场景:ReentrantLock、ReentrantReadWriteLock.WriteLock
核心方法:
// 获取资源,失败则进入队列阻塞
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 释放资源,唤醒后继节点
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
4.2 共享模式(Shared)
特点:多个线程可同时获取资源,就像电影院可以多人同时入场,但有座位数限制。
应用场景:Semaphore、CountDownLatch
核心方法:
// 共享式获取资源
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 共享式释放资源,传播唤醒信号
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
5. 深入源码:AQS如何实现高性能加锁
AQS底层加锁、释放锁,都是大量基于CAS操作来实现的。让我们看看ReentrantLock的非公平锁实现:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
关键点解析:
-
compareAndSetState(0, 1):判断state是否为0,如果是代表没人加过锁,把这个state设置为1
-
CAS可以无锁化保证一个数值修改的原子性,底层基于Unsafe实现,通过CPU指令实现原子性的CAS操作
-
如果CAS成功,设置当前线程为独占锁持有者;如果失败,调用acquire(1)进入排队等待
5.1 可重入锁的实现原理
如果同一个线程可重入加锁,compareAndSetState方法返回一定是false,此时会执行acquire(1),最终进入nonfairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 关键:判断当前线程是否已经是锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 累加state值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可重入机制:当线程尝试获取锁时,如果发现state不为0,但当前线程就是锁持有者,就直接将state值累加,代表重入次数增加。释放锁时,每次释放将state减1,直到减到0才真正释放锁。
6. 线程排队与唤醒的完整流程
当线程加锁失败时,AQS会执行以下完整流程:
6.1 封装节点并入队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 完整入队操作
return node;
}
6.2 阻塞等待
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 如果是第一个等待节点,再次尝试加锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 尝试加锁失败,挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
6.3 释放锁并唤醒
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
7. 公平锁 vs 非公平锁:性能与公平的权衡
AQS支持公平锁和非公平锁两种策略,这是面试中的高频考点:
7.1 公平锁(FairSync)
核心特点:严格按照FIFO顺序让队列中的线程获取锁。
关键代码:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平性关键:判断是否有前序等待线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// … 重入逻辑
}
hasQueuedPredecessors()方法会检查队列中是否有比当前线程更早等待的线程,有就返回true,当前线程就不会去抢锁,保证了公平性。
7.2 非公平锁(NonfairSync)
核心特点:允许新来的线程"插队",与队列头节点唤醒的线程竞争。
性能优势:非公平锁在tryAcquire时没有hasQueuedPredecessors()判断,直接尝试CAS抢锁,减少了性能开销。根据测试,在并发量10000时,非公平锁的吞吐量比公平锁高了约20%。

选择建议:
-
非公平锁:适用于任务执行时间短的场景,减少线程切换开销
-
公平锁:适用于需要避免饥饿的敏感任务,如交易系统
8. 读写锁:读多写少场景的优化
ReentrantReadWriteLock是AQS的另一个经典应用,它巧妙利用state变量的高低16位分别表示读锁和写锁:
-
高16位:读锁计数(允许多个线程同时读取)
-
低16位:写锁计数(同一时间只能有一个线程写入)

8.1 读写锁的互斥规则:
-
读锁 → 写锁:互斥(有读锁时不能加写锁)
-
写锁 → 读锁:互斥(有写锁时不能加读锁)
-
写锁 → 写锁:互斥(同一时间只能有一个写锁)
-
读锁 → 读锁:不互斥(允许多个读锁同时存在)
这种设计在读多写少的场景下能显著提升并发性能,如缓存系统、配置表等。

9. AQS的性能优化策略
现代AQS实现包含多项优化策略,确保在高并发场景下的性能:
9.1 快速路径(Fast Path)
在acquire()中先直接尝试获取锁,避免不必要的入队操作。
9.2 自旋优化
线程在阻塞前进行有限次自旋,减少上下文切换开销。
9.3 队列跳跃
addWaiter()中的CAS快速入队尝试,提高入队效率。
9.4 唤醒传播
共享模式下的doReleaseShared()级联唤醒,提高唤醒效率。
9.5 避免伪共享
通过缓存行填充优化节点内存布局,减少CPU缓存失效。
10. 如何基于AQS实现自定义同步器?
AQS采用模板方法模式,让开发者可以轻松实现自定义同步器。以下是实现步骤:
10.1 继承AQS
创建一个新的类,继承AbstractQueuedSynchronizer。
10.2 定义同步状态
根据需求定义state变量的语义。
10.3 实现核心方法
根据同步器类型实现相应方法:
独占模式:
protected boolean tryAcquire(int arg) {
// 定义获取独占锁的逻辑
}
protected boolean tryRelease(int arg) {
// 定义释放独占锁的逻辑
}
共享模式:
protected int tryAcquireShared(int arg) {
// 定义获取共享锁的逻辑
}
protected boolean tryReleaseShared(int arg) {
// 定义释放共享锁的逻辑
}
10.4 提供外部接口
实现Lock或其他接口,对外提供加锁、解锁等方法。
11. AQS vs Synchronized:如何选择?
|
实现方式 |
Java代码实现 |
JVM内置实现 |
|
锁获取方式 |
可中断、可超时 |
不可中断 |
|
公平性 |
可支持公平/非公平 |
非公平 |
|
条件队列 |
支持多个条件 |
单个条件 |
|
性能 |
高竞争下表现更好 |
低竞争下更优 |
选择建议:
-
ReentrantLock优先:高竞争(线程数>32)、需要超时/中断/公平性、有并发编程经验
-
Synchronized优先:低竞争(线程数<8)、基础同步需求、新手团队或快速开发
12. 总结:AQS的设计哲学
AQS的成功在于其"抽象与复用"的思想:
-
标准化同步框架:将复杂线程调度抽象为状态管理与队列操作
-
高性能保证:通过CAS和volatile实现无锁化竞争
-
灵活扩展:模板方法模式支持多样化同步器实现
理解AQS不仅有助于我们更好地使用Java并发工具,更能提升构建高性能、高可靠性并发组件的能力。正如AQS的设计者Doug Lea所言:"并发问题的本质是对资源的合理调度"。






