ReentrantLock 到底怎么排队的?AQS 源码拆解

21 阅读4分钟

synchronized 用得好好的,为什么要用 ReentrantLock?"可中断、可超时、公平锁、多条件变量"这些名词背得滚瓜烂熟,但一问底层怎么实现的,就只剩"基于 AQS"五个字。

这篇把 ReentrantLock 和它背后的 AQS(AbstractQueuedSynchronizer)从源码层面拆开,看完你就知道锁到底是怎么排队的、公平和非公平差在哪、Condition 是怎么做到"精准唤醒"的。

一、AQS 是什么:一把锁的核心三件套

AQS 本质上是一个同步器框架,核心只有三样东西:

// 1. 状态位:volatile int state(0=没锁,>0=被持锁)
private volatile int state;

// 2. CLH 变体双向队列(排队等锁的线程)
static final class Node {
    volatile Thread thread;   // 排队的线程
    volatile Node prev;        // 前驱
    volatile Node next;        // 后继
    volatile int waitStatus;   // 节点状态
}
// head 指向队首(持有锁或即将持有),tail 指向队尾
private transient volatile Node head;
private transient volatile Node tail;

一句话:state 记录"锁被占了几次",队列记录"谁在等"。所有同步器(锁、信号量、栅栏)都复用这套骨架。

二、公平锁 vs 非公平锁:只差一行

ReentrantLock 默认非公平。看源码:

// 非公平锁加锁
final void lock() {
    if (compareAndSetState(0, 1))   // 来了就抢(不管队列里有没有人等)
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

// 公平锁加锁
final void lock() {
    acquire(1);
}

// acquire → tryAcquire
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 公平锁:必须队列为空才抢;非公平锁:直接抢
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    ...
}

非公平:新线程来先 CAS 抢一次,抢不到才排队;公平:先看队列有没有人等,有人就老实排队。所以非公平吞吐高(减少上下文切换),但可能"插队",饥饿风险小但长期排队者可能晚一点拿到。

三、获取锁失败怎么办:acquire 全流程

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&                 // 1. 尝试获取(公平/非公平差异在这)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 失败→入队→排队自旋
        Thread.currentThread().interrupt();
}

拆解:

第一步:入队(addWaiter)

private Node addWaiter(Node mode) {
    Node node = new Node(mode);   // 包装当前线程
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {  // CAS 挂到队尾
            pred.next = node;
            return node;
        }
    }
    enq(node);   // 队列为空或 CAS 失败,用自旋入队
    return node;
}

第二步:排队等待(acquireQueued)

final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    for (;;) {
        final Node p = node.predecessor();
        if (p == head && tryAcquire(arg)) {   // 前驱是队首 → 再抢一次
            setHead(node);                     // 拿到锁,自己变队首
            p.next = null;                     // 出队
            return interrupted;
        }
        // 没拿到 → 挂起线程(park),等前驱唤醒
        if (shouldParkAfterFailedAcquire(p, node))
            interrupted |= parkAndCheckInterrupt();
    }
}

核心:每个节点在自己的前驱是 head 时才有资格尝试获取锁——这就是队列的 FIFO 特性(非公平也只在入队前插队一次,入队后就老实排队)。

四、释放锁:唤醒后继

public final boolean release(int arg) {
    if (tryRelease(arg)) {          // state 减到 0 才算释放
        Node h = head;
        if (h != null) {
            LockSupport.unpark(h.next.thread);  // 唤醒队首的下一个
        }
        return true;
    }
    return false;
}

关键tryRelease 里 state 减到 0 才真正释放(可重入时 state > 0 只是减一,锁还在)。唤醒的永远是 head 的下一个节点

五、可重入:state 是怎么玩出花的

// 可重入:同一线程再次 lock
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { ... }                       // 无人持有 → 直接拿
    else if (current == getExclusiveOwnerThread()) {  // 自己已持有
        int nextc = c + acquires;             // state + 1
        setState(nextc);                        // 重入+1
        return true;
    }
    return false;  // 别人持有 → 失败
}

state 就是重入计数:0=无锁,1=持有一层,2=同一线程重入两次……每次 unlock 减一,减到 0 才真正释放唤醒后继。

六、Condition:精准唤醒怎么实现

synchronized 只有 wait/notify 一个等待集;ReentrantLock 可以 new 多个 Condition,每个 Condition 一条独立等待队列

Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
  • condition.await():当前线程释放锁,加入该 condition 的等待队列,挂起
  • condition.signal():把等待队列队首一个节点移到锁的同步队列,等它重新竞争
  • signalAll():全部移到同步队列

对比 synchronized:

能力synchronizedReentrantLock + Condition
锁释放自动否(finally unlock)
可重入
中断响应
超时等待tryLock(3, SECONDS)
条件变量1 个多个 Condition
公平性非公平可配置

七、总结

  1. AQS 三件套:state + 双向等待队列 + CAS,是所有同步工具的地基。
  2. 公平/非公平:公平 = 入队前看队列;非公平 = 先 CAS 抢一次。入队后都 FIFO。
  3. 获取流程:tryAcquire → 入队 → 自旋检查前驱是 head → park。
  4. 释放流程:state 减到 0 → unpark 后继。
  5. 重入:state 就是重入计数,减到 0 才真正释放。
  6. Condition:每个 Condition 一条等待队列,精准唤醒。

下一篇:synchronized 和 volatile 的底层原理,JMM 三大特性是怎么落到 CPU 缓存上的。