前面我们讲了 synchronized 关键字,从对象头、Mark Word 到锁升级的全过程,理解了 JVM 内置锁的强大与局限。今天,我们将目光转向 Java 并发包中另一个核心组件 ——AbstractQueuedSynchronizer(AQS),并以 ReentrantLock 为例, 聊一聊“显式锁” 的底层世界。


一、AQS:Java 并发的 “幕后基石”

如果说 synchronized 是 JVM 为我们提供的 “黑盒” 锁,那么 AQS 就是构建整个 java.util.concurrent(JUC)包的 “白盒” 框架。我们熟知的 ReentrantLockCountDownLatchSemaphoreCyclicBarrier 等,无一不是基于 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 从入队到唤醒的流程

  1. 封装入队addWaiter(Node.EXCLUSIVE)。将当前线程封装成 Node,加入队列尾部(CAS 保证原子性)。
  2. 自旋阻塞acquireQueued(node, arg)。节点进入队列后,不会立即阻塞,而是进行自旋(循环)。
    1. 自旋条件:只要前驱节点是头节点,就会再次尝试 tryAcquire
    2. 阻塞时机:尝试失败后,判断是否需要阻塞(通过 shouldParkAfterFailedAcquire 将前驱状态设为 SIGNAL),然后调用 parkAndCheckInterrupt() 挂起线程。
  3. 唤醒出队:当锁释放(unlock)时,头节点的后继节点会被唤醒(unpark),被唤醒的节点再次进入自旋,尝试获取锁。

二、ReentrantLock:AQS 的经典实现

ReentrantLock 是 AQS 最典型的应用,它是一个可重入的互斥锁。与 synchronized 相比,它提供了更灵活的 API 和更强的控制力。

1. ReentrantLock 的类结构

ReentrantLock 内部定义了一个继承自 AQS 的抽象内部类 SyncSync 又有两个具体实现:FairSync(公平锁)和 NonfairSync(非公平锁)。

  • 公平锁(FairSync):严格按照线程在队列中的等待顺序(FIFO)分配锁,保证 “先到先得”,避免线程饥饿。
  • 非公平锁(NonfairSync):新线程尝试获取锁时,可以直接 “插队” 竞争,无需排队。这减少了上下文切换,性能更高,但可能导致线程饥饿。

在计算机系统中,非公平策略通常性能更优。ReentrantLock 默认使用非公平锁。

ReentrantLock 的非公平锁为什么比公平锁性能高?

非公平锁在获取锁时会直接尝试 CAS,避免了进入队列的开销;同时减少了线程挂起和唤醒的上下文切换。

NonfairSynclock() 方法中,一上来就会直接尝试 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

我们来逐行解析公平锁 FairSynctryAcquire 方法,这是理解锁获取逻辑的关键。

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. 获取锁失败
}
  1. int c = getState();:首先读取 volatile 变量 state,这保证了我们看到的锁状态是最新的。
  2. if (c == 0):如果锁未被持有,进入分支 1。
    • isFirst(current):这是公平锁的灵魂。它检查当前线程是否是等待队列中的第一个节点。只有是第一个,才有资格尝试获取锁,严格保证了 FIFO 顺序。
    • compareAndSetState(0, acquires):使用 CAS(Compare-and-Swap)原子操作,尝试将 state 从 0 更新为 1。这确保了在多线程并发竞争时,只有一个线程能成功。
    • setExclusiveOwnerThread(current):如果 CAS 成功,就将当前线程标记为锁的独占持有者。
  3. else if (current == getExclusiveOwnerThread()):如果锁已被持有,且持有者就是当前线程,进入分支 2(重入)。
    • int nextc = c + acquires;:将重入次数加 1。
    • setState(nextc);:直接更新 state。因为这里已经确定是持有锁的线程在操作,不存在并发竞争,所以不需要 CAS。
  4. 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 是如何协同工作,构建出高效的同步组件的。

喜欢这篇内容的,可以给我点赞支持一下~

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐