面试必问、源码必看、并发编程绕不开的核心
前言
在 Java 并发编程里,有一个绕不开的名字 ------ AQS。
它是 java.util.concurrent 包下大多数并发工具的底层实现。我们常用的 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier,全部都是基于 AQS 实现的。
很多读者面试时被问到"讲讲 AQS",要么答不上来,要么答得很零散。今天这篇文章,我带大家彻底搞懂它。
一、AQS 是什么
AQS 全称是 AbstractQueuedSynchronizer,翻译过来就是"抽象队列同步器"。
光看名字可能有点抽象,我们先记住它的核心三要素:
- 一个状态变量 ------ 用来表示锁是否被占用
- 一个队列 ------ 用来存放等待锁的线程
- 一套模板方法 ------ 封装了获取锁、释放锁的通用逻辑
AQS 把这些公共逻辑都写好了,子类只需要实现几个关键方法,就能完成一个锁。这就好像我们盖房子,AQS 提供了骨架,我们只需要往里面填砖就能建成。
二、核心结构
我们先看一下 AQS 的核心属性:
java
public abstract class AbstractQueuedSynchronizer {
// 同步状态,0 表示未被占用,>0 表示被占用
private volatile int state;
// 等待队列的头结点
private transient Node head;
// 等待队列的尾结点
private transient Node tail;
}
2.1 等待队列是什么
AQS 内部维护了一个 双向 FIFO 队列,所有竞争锁失败的线程都会被放到这个队列里排队,等待锁释放后被唤醒。
想象一下去银行办业务,只有一个窗口,很多人在门口排队。窗口就是"锁",排队的人就是队列里的 Node。
队列结构大概长这样:

2.2 Node 节点
队列里的每个节点就是一个 Node,包含以下关键字段:
java
static final class Node {
// 节点状态,有以下几种值:
// CANCELLED = 1 线程已取消等待
// SIGNAL = -1 后继节点需要被唤醒
// CONDITION = -2 线程在等待条件
// PROPAGATE = -3 共享模式下传播
int waitStatus;
// 前驱和后继节点(双向链表)
Node prev;
Node next;
// 关联的线程(这个节点代表哪个线程)
Thread thread;
// 等待模式:SHARED(共享)或 EXCLUSIVE(独占)
Node nextWaiter;
}
三、两种同步模式
AQS 支持两种同步模式,理解这一点很关键:
3.1 独占模式
同一时刻只有一个线程 能获取锁。比如我们常见的 ReentrantLock。
就像一个厕所只有一个坑位,一次只能进一个人。
3.2 共享模式
同一时刻多个线程 可以同时获取锁。比如 CountDownLatch、Semaphore。
就像一个停车场有多个车位,可以同时停多辆车。
这就是为什么 ReentrantLock 只能单线程通过,而 CountDownLatch 可以让多个线程同时通过的底层原因。
四、核心方法
AQS 采用模板方法模式 ,定义了一套获取/释放锁的完整流程,子类只需要实现 tryAcquire 和 tryRelease 即可。
4.1 子类需要实现的方法
java
// 独占模式:尝试获取锁
// 返回 true 表示获取成功,false 表示失败
protected boolean tryAcquire(int arg)
// 独占模式:尝试释放锁
// 返回 true 表示释放成功
protected boolean tryRelease(int arg)
// 共享模式:尝试获取共享锁
// 返回负数表示失败,0 表示成功但没有剩余资源,正数表示成功且还有剩余资源
protected int tryAcquireShared(int arg)
// 共享模式:尝试释放共享锁
// 返回 true 表示释放成功
protected boolean tryReleaseShared(int arg)
// 检查是否被当前线程独占(用于条件变量的判断)
protected boolean isHeldExclusively()
4.2 模板方法(对外暴露)
这些方法是给外部调用的,内部已经封装好了完整的逻辑:
java
// 独占获取(最常用)
public final void acquire(int arg)
// 独占获取,可中断(线程在等待时可以被 interrupt)
public final void acquireInterruptibly(int arg)
// 独占获取,带超时
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
// 独占释放
public final void release(int arg)
// 共享获取
public final void acquireShared(int arg)
// 共享释放
public final void releaseShared(int arg)
五、完整流程分析
5.1 获取锁的全过程
以 acquire(1) 为例,假设我们要获取一个独占锁,完整流程是这样的:
第一步:tryAcquire 尝试直接获取
调用 tryAcquire(1) 尝试直接获取锁。这个方法是由子类实现的。
- 如果返回
true,说明获取成功,线程直接持有锁,流程结束 - 如果返回
false,说明锁被别人占用了,继续往下走
第二步:addWaiter 入队
如果获取失败,需要把当前线程包装成 Node,加入等待队列。
java
// 简化的入队逻辑
private Node addWaiter(Node mode) {
// 把当前线程包装成 Node
Node node = new Node(Thread.currentThread(), mode);
// 快速 CAS 入队(尝试直接加到队尾)
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果快速入队失败,走完整入队流程
enq(node);
return node;
}
第三步:acquireQueued 自旋等待
加入队列后,线程并不会一直阻塞,而是先自旋尝试获取几次锁。如果实在获取不到,再阻塞。
为什么要这样设计呢?因为阻塞和唤醒线程需要调用操作系统内核,有一定的开销。如果锁很快释放,自旋等待的效率更高。
java
// 简化的自旋等待逻辑
final boolean acquireQueued(Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点
Node p = node.predecessor();
// 如果前驱是 head,说明我们是队列第一个等待者
// 再次尝试获取锁
if (p == head && tryAcquire(arg)) {
// 获取成功把自己设置为 head
setHead(node);
p.next = null; // 方便 GC
failed = false;
return interrupted;
}
// 获取失败,检查是否需要阻塞
// 如果前驱节点状态是 SIGNAL,说明可以安全阻塞
if (shouldParkAfterFailedAcquire(p, node)) {
// 阻塞当前线程
LockSupport.park(this);
// 被唤醒后检查中断状态
if (Thread.interrupted()) {
interrupted = true;
}
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
用一张图来表示整个流程:
尝试直接获取锁"] --> B{"成功?"} B -- 是 --> C["返回
持有锁"] B -- 否 --> D["addWaiter
入队"] D --> E["acquireQueued
自旋等待"] E --> F{"获取成功?"} F -- 是 --> G["返回"] F -- 否 --> H["park()
阻塞"] H --> E
5.2 释放锁的全过程
释放锁相对简单一些:
markdown
1. 调用 tryRelease(1) 尝试释放锁
├── 失败 → 直接返回
└── 成功 → 继续
2. 唤醒后继节点(unparkSuccessor)
└── 后继节点被唤醒后,会继续自旋尝试获取锁
六、实战:手写一个简单锁
理解了 AQS,我们自己来实现一个简单的互斥锁:
java
public class SimpleLock extends AbstractQueuedSynchronizer {
// 尝试获取锁
@Override
protected boolean tryAcquire(int arg) {
// CAS 尝试把 state 从 0 改为 1
// compareAndSetState 是 AQS 提供的原子操作
return compareAndSetState(0, 1);
}
// 尝试释放锁
@Override
protected boolean tryRelease(int arg) {
// 直接把 state 设为 0
setState(0);
return true;
}
// 检查是否被当前线程独占
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 对外暴露的获取锁方法
public void lock() {
acquire(1);
}
// 对外暴露的释放锁方法
public void unlock() {
release(1);
}
}
测试一下:
java
public class TestSimpleLock {
private static SimpleLock lock = new SimpleLock();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取了锁");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了锁");
}
}, "线程-" + i).start();
}
}
}
输出结果:
线程-0 获取了锁
线程-0 释放了锁
线程-1 获取了锁
线程-1 释放了锁
线程-2 获取了锁
线程-2 释放了锁
线程-3 获取了锁
线程-3 释放了锁
线程-4 获取了锁
线程-4 释放了锁
可以看到,线程是依次获取锁的,不会出现并发问题。这就是 AQS 的威力 ------ 才 20 行代码,一个锁就写好了!
七、公平锁 vs 非公平锁
ReentrantLock 本身不实现锁的逻辑,它只是调用 AQS 的模板方法。但它通过传入不同的 Sync 实现类来区分公平和非公平:
- 非公平锁:插队!直接尝试 CAS 获取锁,不管队列里有没有人在等
- 公平锁:排队!先看看队列里有没有人,有人就老实在后面排队
7.1 非公平锁
java
// 非公平锁的 tryAcquire 实现
protected boolean tryAcquire(int arg) {
// 直接 CAS 尝试获取,不看队列
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
7.2 公平锁
java
// 公平锁的 tryAcquire 实现
protected boolean tryAcquire(int arg) {
// 先看看有没有人在排队
if (!hasQueuedPredecessors()) {
// 没人排队,才尝试获取
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
7.3 怎么选择
| 锁 | 优点 | 缺点 |
|---|---|---|
| 非公平锁 | 吞吐量高,可能插队成功 | 可能产生线程饥饿 |
| 公平锁 | 保证先来后到 | 吞吐量低,需要维护队列 |
ReentrantLock 默认是非公平的,因为非公平锁的吞吐量通常更高。
八、常见的 AQS 实现类
理解了 AQS,你会发现 JUC 包下大部分并发工具都是基于它实现的:
| 类 | 模式 | 用途 |
|---|---|---|
| ReentrantLock | 独占 | 可重入互斥锁 |
| ReentrantReadWriteLock | 独占/共享 | 读写锁,读写分离 |
| CountDownLatch | 共享 | 倒计时,多线程汇合 |
| CyclicBarrier | 共享 | 循环屏障 |
| Semaphore | 共享 | 信号量,限流 |
| Phaser | 共享 | 多阶段同步 |