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

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

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

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

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

java 复制代码
// 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 默认非公平。看源码:

java 复制代码
// 非公平锁加锁
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 全流程

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

拆解:

第一步:入队(addWaiter)

java 复制代码
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)

java 复制代码
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 特性(非公平也只在入队前插队一次,入队后就老实排队)。

四、释放锁:唤醒后继

java 复制代码
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 是怎么玩出花的

java 复制代码
// 可重入:同一线程再次 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 一条独立等待队列

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

对比 synchronized:

能力 synchronized ReentrantLock + 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 缓存上的。

相关推荐
Zane19941 小时前
ArrayList 插入慢,LinkedList 一定快吗
java·后端
吃饱了得干活1 小时前
从类爆炸到协作——DDD战略设计登场
java·后端·架构
wno7041 小时前
Spring Boot异常处理
java·spring boot·后端
君顾12 小时前
智慧场馆解决方案小程序系统开发实战指南
java·开发语言·智慧场馆
学习星球2 小时前
【LeetCode算法题精讲】二分查找精讲
java·数据结构·算法·leetcode·职场和发展·图搜索
Escalating_xu2 小时前
【Linux线程同步】从数据竞争到 mutex、条件变量与生产者消费者(上篇)
android·java·linux
承渊政道2 小时前
10:30提交预约会不会撞上10:00的会议?我用飞算JavaAI3.9.1和3.9.9跑了四个时间段
java·springboot·ai编程·飞算javaai·java代码生成
SL_staff2 小时前
风控规则如何从硬编码解耦?我们在 JVS-Rules 中的实践与思考
java·spring boot·设计模式
数据库技术讲堂2 小时前
从 GitOps 到数据库变更:NineData 如何打通 CI/CD 的数据库治理链路
java·数据库·ci/cd