以ReentrantLock为例讲讲AQS原理
本文讲解AQS框架如何支撑ReentrantLock。AQS提供状态管理和线程排队的核心机制,而ReentrantLock则定义了状态(重入次数)的具体含义和获取/释放锁的业务逻辑,两者协作实现了高效的同步。
以 ReentrantLock 为例来讲解 AQS 的设计原理是最好的方式,因为它完美地展示了 AQS 是如何被“使用”的。
ReentrantLock 本身不包含复杂的同步逻辑,它的核心功能完全委托给了其内部的一个帮助类——Sync,而 Sync 类正是继承自 AQS。
ReentrantLock 有两种模式:公平锁(FairSync)和非公平锁(NonfairSync),它们都是 Sync 的子类。
我们来一步步拆解 ReentrantLock 是如何利用 AQS 的三大核心组件(State、FIFO 队列、独占模式)来实现自己的功能的。
1. ReentrantLock 如何定义 "State"
在 ReentrantLock 中,AQS 的 int state 变量被用来表示锁的重入次数。
state == 0:表示锁当前未被任何线程持有(unlocked状态)。state > 0:表示锁已被某个线程持有(locked状态)。state的值就是该线程获取锁的次数。
但是,仅有 state 还不够! AQS 只知道状态值,但不知道这个锁是被哪个线程持有的。因此,ReentrantLock 的 Sync 类自己额外增加了一个成员变量:
// 在 AQS 的子类 Sync 中
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
// 用于记录当前持有锁的线程
private transient Thread exclusiveOwnerThread;
// ...
}
所以,ReentrantLock 的完整状态由两部分组成:
state(来自 AQS):锁的重入计数。exclusiveOwnerThread(来自 Sync):当前锁的持有者。
2. ReentrantLock 如何实现 tryAcquire (获取锁的核心逻辑)
这是 ReentrantLock 实现其“可重入”和“独占”特性的关键。当一个线程调用 lock() 时,AQS 的 acquire(1) 方法会调用 Sync 重写的 tryAcquire(1) 方法。
我们以非公平锁(NonfairSync)的 tryAcquire 为例,它的逻辑非常清晰:
// NonfairSync.java
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取 AQS 的 state
if (c == 0) { // Case 1: 锁是自由的
// 尝试用 CAS 将 state 从 0 修改为 1
if (compareAndSetState(0, 1)) {
// 如果成功,说明抢锁成功!
setExclusiveOwnerThread(current); // 记录自己是锁的持有者
return true; // 成功获取
}
}
else if (current == getExclusiveOwnerThread()) { // Case 2: 锁被当前线程持有
// 这就是“可重入”的体现
int nextc = c + 1; // 重入次数 +1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); // 直接设置 state,无需 CAS,因为没有竞争
return true; // 成功获取
}
// Case 3: 锁被其他线程持有
return false; // 获取失败
}
逻辑解读:
情况一:锁是自由的 (
state == 0)- 线程尝试通过 CAS (Compare-And-Set) 操作将
state从 0 改为 1。 - CAS 成功:意味着它在竞争中胜出,成功抢到了锁。于是它将
exclusiveOwnerThread设置为自己,并返回true。 - CAS 失败:意味着在它检查到
state为0 和尝试修改的瞬间,锁被其他线程抢走了。进入情况三。
- 线程尝试通过 CAS (Compare-And-Set) 操作将
情况二:锁已被自己持有 (
current == exclusiveOwnerThread)- 这正是“可重入”的核心。如果发现锁的持有者就是自己,那么直接将
state加 1 即可,表示重入深度增加。 - 这里不需要 CAS,因为当前线程已经持有了锁,其他线程都在等待队列里,不会有线程来和它竞争修改
state。 - 返回
true。
- 这正是“可重入”的核心。如果发现锁的持有者就是自己,那么直接将
情况三:锁被其他线程持有
- 直接返回
false。
- 直接返回
当 tryAcquire 返回 false 时,AQS 框架接管一切:自动将当前线程打包成 Node,加入到等待队列的末尾,并最终挂起(park)线程。
3. ReentrantLock 如何实现 tryRelease (释放锁的核心逻辑)
当线程调用 unlock() 时,AQS 的 release(1) 方法会调用 Sync 重写的 tryRelease(1) 方法。
// Sync.java
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // state 减 1
// 安全检查:如果不是锁的持有者尝试解锁,就抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // Case 1: state 减到 0 了
// 锁被完全释放
free = true;
setExclusiveOwnerThread(null); // 清除持有者
}
// Case 2: state > 0,锁还未完全释放(因为是重入的)
// 什么也不做,等待下一次 unlock
setState(c); // 设置新的 state 值
return free; // 返回锁是否已完全释放
}
逻辑解读:
- 首先进行安全检查,确保是锁的持有者线程在调用
unlock()。 - 将
state减 1。 - 如果
state减到 0:这表示所有重入的lock()都已经被对应的unlock()平衡了,锁现在是真正地被释放了。此时,需要将exclusiveOwnerThread设为null,并返回true。 - 如果
state仍然大于 0:这表示这只是一个重入的unlock(),锁仍然被当前线程持有。此时返回false。
当 tryRelease 返回 true 时,AQS 框架知道锁已经完全释放,它会自动去唤醒(unpark)等待队列中的头节点的下一个节点(即等待时间最长的线程)。
4. 公平锁 vs 非公平锁的区别
这个区别,完美地体现在了 tryAcquire 的细微不同上。
非公平锁 (
NonfairSync):如上文分析,新来的线程不管队列中是否有等待者,都会先尝试 CAS 抢锁。这种“插队”行为提高了吞吐量,但可能导致队列中的线程饿死。公平锁 (
FairSync):它的tryAcquire在非公平锁的基础上,增加了一个前置条件。java// FairSync.java protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 关键区别在这里! // hasQueuedPredecessors() 检查等待队列中是否有比我更早的等待者 if (!hasQueuedPredecessors() && compareAndSetState(0, 1)) { setExclusiveOwnerThread(current); return true; } } // ... 后面的重入逻辑和非公平锁一样 ... return false; }公平锁的关键:在尝试 CAS 抢锁之前,它会调用 AQS 提供的
hasQueuedPredecessors()方法检查等待队列。如果发现队列中已经有其他线程在排队,它就不会去尝试抢锁,而是直接返回false,乖乖去队尾排队。这保证了先到先得的公平性。
总结:ReentrantLock 与 AQS 的协作关系
| ReentrantLock 关注的 "策略" (Policy) | AQS 提供的 "机制" (Mechanism) |
|---|---|
状态含义: state 代表重入次数,exclusiveOwnerThread 代表持有者。 |
状态管理: 提供 volatile int state 和 getState, setState, compareAndSetState 原子方法。 |
获取逻辑 (tryAcquire): 判断是否自由,是否是自己重入,是否需要排队(公平锁)。 |
获取流程 (acquire): 调用 tryAcquire,如果失败,则自动将线程入队、阻塞。 |
释放逻辑 (tryRelease): 减少重入次数,如果减到0则完全释放。 |
释放流程 (release): 调用 tryRelease,如果成功,则自动唤醒队列中的下一个线程。 |
公平性策略: 通过 hasQueuedPredecessors() 方法来决定是否“插队”。 |
队列管理: 提供 hasQueuedPredecessors() 等方法,维护一个先进先出的等待队列。 |
通过这个例子,我们可以清晰地看到 AQS 的设计精髓:
AQS 搭建了同步器的骨架(排队、阻塞、唤醒),而具体的同步器如 ReentrantLock 则负责填充血肉(定义状态的含义和状态转换的规则)。 这种关注点分离的设计,使得构建高效、健壮的同步组件变得异常简单。