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 缓存上的。

相关推荐
时间静止不是简史4 小时前
生产事故排查:Redis 宕机导致补偿队列消费线程死亡,通话录音未上送业务平台
java·redis
2301_32241428047 小时前
基于SpringBoot+Vue的休闲零食超市管理系统的设计与实现59004-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·vue.js·spring boot·redis·mysql·决策树·推荐算法
金銀銅鐵9 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
福建佰胜张工10 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop10 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x10 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事11 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客11 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn18811 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn12 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc