并发编程(十三):AQS 与 ReentrantLock 从原理到源码
前面我们讲了 synchronized 关键字,从对象头、Mark Word 到锁升级的全过程,理解了 JVM 内置锁的强大与局限。今天,我们将目光转向 Java 并发包中另一个核心组件 ——AbstractQueuedSynchronizer(AQS),并以 ReentrantLock 为例, 聊一聊“显式锁” 的底层世界。
一、AQS:Java 并发的 “幕后基石”
如果说 synchronized 是 JVM 为我们提供的 “黑盒” 锁,那么 AQS 就是构建整个 java.util.concurrent(JUC)包的 “白盒” 框架。我们熟知的 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 等,无一不是基于 AQS 构建的。
1. AQS 的核心设计思想
AQS 的核心可以用一句话概括:用一个 volatile 变量维护同步状态,用一个 FIFO 队列管理等待线程。
同步状态(state):AQS 使用一个 private volatile int state; 变量来表示当前的同步状态。这个 volatile 变量是整个机制的基石,它保证了状态变化在多线程之间的可见性和有序性。
AQS 中的 state 为什么要用 volatile 修饰?
保证多线程间的可见性,配合 CAS 实现无锁化的状态更新。
在 ReentrantLock 中,state 代表锁的重入次数。state=0 表示锁未被持有,state>0 表示锁被持有,值越大,重入次数越多。
在 CountDownLatch 中,state 代表需要倒数的计数器值。
FIFO 等待队列:当线程获取锁失败时,AQS 会将其包装成一个 Node 节点,加入到一个双向链表(CLH 队列的变种)中进行排队等待。这个队列严格遵循先进先出(FIFO)原则,保证了线程竞争的公平性(在公平锁模式下)。
2. AQS 的模板方法模式
AQS 使用了经典的模板方法模式。它定义了获取和释放同步状态的顶层逻辑(如 acquire()、release()),而将具体的 “尝试获取” 和 “尝试释放” 逻辑(如 tryAcquire()、tryRelease())留给子类去实现。
这种设计使得 AQS 成为一个高度可扩展的框架。不同的同步器,只需要实现这几个关键的钩子方法,就能拥有一套完整的线程排队和阻塞机制。
ReentrantLock 的重入性是如何实现的?
tryAcquire中判断当前线程是否为独占线程,如果是,直接将 state +1;tryRelease中只有 state -1 为 0 时,才真正释放锁。
3. AQS 从入队到唤醒的流程
- 封装入队:
addWaiter(Node.EXCLUSIVE)。将当前线程封装成 Node,加入队列尾部(CAS 保证原子性)。 - 自旋阻塞:
acquireQueued(node, arg)。节点进入队列后,不会立即阻塞,而是进行自旋(循环)。- 自旋条件:只要前驱节点是头节点,就会再次尝试
tryAcquire。 - 阻塞时机:尝试失败后,判断是否需要阻塞(通过
shouldParkAfterFailedAcquire将前驱状态设为 SIGNAL),然后调用parkAndCheckInterrupt()挂起线程。
- 自旋条件:只要前驱节点是头节点,就会再次尝试
- 唤醒出队:当锁释放(
unlock)时,头节点的后继节点会被唤醒(unpark),被唤醒的节点再次进入自旋,尝试获取锁。
二、ReentrantLock:AQS 的经典实现
ReentrantLock 是 AQS 最典型的应用,它是一个可重入的互斥锁。与 synchronized 相比,它提供了更灵活的 API 和更强的控制力。
1. ReentrantLock 的类结构
ReentrantLock 内部定义了一个继承自 AQS 的抽象内部类 Sync。Sync 又有两个具体实现:FairSync(公平锁)和 NonfairSync(非公平锁)。
- 公平锁(FairSync):严格按照线程在队列中的等待顺序(FIFO)分配锁,保证 “先到先得”,避免线程饥饿。
- 非公平锁(NonfairSync):新线程尝试获取锁时,可以直接 “插队” 竞争,无需排队。这减少了上下文切换,性能更高,但可能导致线程饥饿。
在计算机系统中,非公平策略通常性能更优。ReentrantLock 默认使用非公平锁。
ReentrantLock 的非公平锁为什么比公平锁性能高?
非公平锁在获取锁时会直接尝试 CAS,避免了进入队列的开销;同时减少了线程挂起和唤醒的上下文切换。
在
NonfairSync的lock()方法中,一上来就会直接尝试 CAS 修改state。// 非公平锁的 lock 方法片段 final void lock() { if (compareAndSetState(0, 1)) // 第一步:直接插队抢锁 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 抢不到再走 AQS 队列流程 }公平锁:先看队列,再去抢。
非公平锁:先抢一把,抢不到再去排队。
性能原因:非公平锁减少了线程从 “挂起” 到 “唤醒” 的上下文切换开销
2. 核心 API 与使用规范
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
// ReentrantLock lock = new ReentrantLock(true); // 公平锁
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁,在 finally 块中
}
lock():阻塞式获取锁,获取不到则进入队列等待。unlock():释放锁。必须在finally块中调用,确保锁一定会被释放,避免死锁。
三、源码:公平锁的 tryAcquire
我们来逐行解析公平锁 FairSync 的 tryAcquire 方法,这是理解锁获取逻辑的关键。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 1. 读取 volatile 变量 state
if (c == 0) {
// 分支1:锁当前未被任何线程持有
if (isFirst(current) && // 2. 公平性检查:我是不是队列里的第一个?
compareAndSetState(0, acquires)) { // 3. CAS 原子操作尝试获取锁
setExclusiveOwnerThread(current); // 4. 设置当前线程为锁持有者
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 分支2:锁已被当前线程持有(重入场景)
int nextc = c + acquires; // 5. 重入次数 +1
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); // 6. 直接更新 state,无需 CAS
return true;
}
return false; // 7. 获取锁失败
}
int c = getState();:首先读取volatile变量state,这保证了我们看到的锁状态是最新的。if (c == 0):如果锁未被持有,进入分支 1。isFirst(current):这是公平锁的灵魂。它检查当前线程是否是等待队列中的第一个节点。只有是第一个,才有资格尝试获取锁,严格保证了 FIFO 顺序。compareAndSetState(0, acquires):使用 CAS(Compare-and-Swap)原子操作,尝试将state从 0 更新为 1。这确保了在多线程并发竞争时,只有一个线程能成功。setExclusiveOwnerThread(current):如果 CAS 成功,就将当前线程标记为锁的独占持有者。
else if (current == getExclusiveOwnerThread()):如果锁已被持有,且持有者就是当前线程,进入分支 2(重入)。int nextc = c + acquires;:将重入次数加 1。setState(nextc);:直接更新state。因为这里已经确定是持有锁的线程在操作,不存在并发竞争,所以不需要 CAS。
return false;:如果以上条件都不满足,返回false,表示获取锁失败。AQS 的上层框架会将线程加入队列等待。
四、ReentrantLock vs synchronized:
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现方式 | API 层面,基于 AQS | JVM 内置关键字 |
| 锁获取方式 | 可中断(lockInterruptibly())、可超时(tryLock(long, TimeUnit))、可轮询 |
不可中断、阻塞等待 |
| 公平性 | 支持公平 / 非公平锁 | 非公平锁 |
| 条件队列 | 支持多个 Condition 对象,可分组等待 / 唤醒 |
仅一个等待队列,通过 wait()/notify() |
| 重入性 | 支持重入 | 支持重入 |
| 性能 | 在高并发下,细粒度锁场景性能更优 | 优化后性能也很强,尤其在低竞争下 |
| 使用复杂度 | 需要手动加锁 / 解锁,代码稍繁琐 | 自动管理,使用简单 |
synchronized:简单、自动、可靠,是大多数场景的首选。JVM 对其进行了大量优化(如锁升级),在很多情况下性能并不差。ReentrantLock:提供了synchronized不具备的高级特性,如可中断、超时等待、多个条件队列等。当你需要这些灵活性,或者需要实现自定义的同步器时,它是不二之选。
五、总结
今天,我们揭开了 AQS 的神秘面纱,理解了它作为 “锁的底层” 的核心地位,并通过 ReentrantLock 的源码,看到了 volatile 和 CAS 是如何协同工作,构建出高效的同步组件的。
喜欢这篇内容的,可以给我点赞支持一下~
更多推荐


所有评论(0)