基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

以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 只知道状态值,但不知道这个锁是被哪个线程持有的。因此,ReentrantLockSync 类自己额外增加了一个成员变量:

java
// 在 AQS 的子类 Sync 中
abstract static class Sync extends AbstractQueuedSynchronizer {
    // ...
    // 用于记录当前持有锁的线程
    private transient Thread exclusiveOwnerThread;
    // ...
}

所以,ReentrantLock 的完整状态由两部分组成:

  1. state (来自 AQS):锁的重入计数。
  2. exclusiveOwnerThread (来自 Sync):当前锁的持有者。

2. ReentrantLock 如何实现 tryAcquire (获取锁的核心逻辑)

这是 ReentrantLock 实现其“可重入”和“独占”特性的关键。当一个线程调用 lock() 时,AQS 的 acquire(1) 方法会调用 Sync 重写的 tryAcquire(1) 方法。

我们以非公平锁(NonfairSync)tryAcquire 为例,它的逻辑非常清晰:

java
// 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; // 获取失败
}

逻辑解读:

  1. 情况一:锁是自由的 (state == 0)

    • 线程尝试通过 CAS (Compare-And-Set) 操作将 state 从 0 改为 1。
    • CAS 成功:意味着它在竞争中胜出,成功抢到了锁。于是它将 exclusiveOwnerThread 设置为自己,并返回 true
    • CAS 失败:意味着在它检查到 state为0 和尝试修改的瞬间,锁被其他线程抢走了。进入情况三。
  2. 情况二:锁已被自己持有 (current == exclusiveOwnerThread)

    • 这正是“可重入”的核心。如果发现锁的持有者就是自己,那么直接将 state 加 1 即可,表示重入深度增加。
    • 这里不需要 CAS,因为当前线程已经持有了锁,其他线程都在等待队列里,不会有线程来和它竞争修改 state
    • 返回 true
  3. 情况三:锁被其他线程持有

    • 直接返回 false

tryAcquire 返回 false 时,AQS 框架接管一切:自动将当前线程打包成 Node,加入到等待队列的末尾,并最终挂起(park)线程。

3. ReentrantLock 如何实现 tryRelease (释放锁的核心逻辑)

当线程调用 unlock() 时,AQS 的 release(1) 方法会调用 Sync 重写的 tryRelease(1) 方法。

java
// 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; // 返回锁是否已完全释放
}

逻辑解读:

  1. 首先进行安全检查,确保是锁的持有者线程在调用 unlock()
  2. state 减 1。
  3. 如果 state 减到 0:这表示所有重入的 lock() 都已经被对应的 unlock() 平衡了,锁现在是真正地被释放了。此时,需要将 exclusiveOwnerThread 设为 null,并返回 true
  4. 如果 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 stategetState, setState, compareAndSetState 原子方法。
获取逻辑 (tryAcquire): 判断是否自由,是否是自己重入,是否需要排队(公平锁)。 获取流程 (acquire): 调用 tryAcquire,如果失败,则自动将线程入队、阻塞。
释放逻辑 (tryRelease): 减少重入次数,如果减到0则完全释放。 释放流程 (release): 调用 tryRelease,如果成功,则自动唤醒队列中的下一个线程。
公平性策略: 通过 hasQueuedPredecessors() 方法来决定是否“插队”。 队列管理: 提供 hasQueuedPredecessors() 等方法,维护一个先进先出的等待队列。

通过这个例子,我们可以清晰地看到 AQS 的设计精髓:

AQS 搭建了同步器的骨架(排队、阻塞、唤醒),而具体的同步器如 ReentrantLock 则负责填充血肉(定义状态的含义和状态转换的规则)。 这种关注点分离的设计,使得构建高效、健壮的同步组件变得异常简单。

00:00
00:00