Java并发编程:ReentrantLock与AQS原理剖析

前言

在Java并发编程中,ReentrantLock是一个非常重要的可重入互斥锁,它比synchronized提供了更灵活的锁机制:支持公平/非公平模式、可响应中断、超时等待、多条件变量等。

但真正让ReentrantLock强大的,是其底层依赖的AbstractQueuedSynchronizer(AQS) 框架。AQS是JUC(java.util.concurrent)包的基石,CountDownLatchSemaphoreReentrantReadWriteLock等同步组件都基于它构建。

本文将逐行解析源码,彻底讲清楚:

  • AQS的核心数据结构(state、CLH队列、Node)

  • 非公平锁的完整加锁/解锁流程

  • 公平锁的实现原理及与非公平锁的区别

  • 同步队列的入队、阻塞、唤醒机制

  • 重入锁的实现原理


一、AQS核心架构

1.1 AQS是什么?

AbstractQueuedSynchronizer是一个抽象类,它提供了一个FIFO队列 来管理等待锁的线程,并定义了一个int类型的state作为同步状态。

子类只需要实现tryAcquiretryRelease等钩子方法,即可构造出自己的同步器。

1.2 核心属性

java 复制代码
// AbstractQueuedSynchronizer 中的核心属性

// 同步状态,volatile保证可见性
// 在ReentrantLock中:state=0表示锁空闲,state>0表示锁被持有(值表示重入次数)
private volatile int state;

// 同步队列的头指针(懒加载,初始化时为空)
private transient volatile Node head;

// 同步队列的尾指针
private transient volatile Node tail;
java 复制代码
// AbstractOwnableSynchronizer(AQS的父类)中的属性

// 记录当前持有锁的线程(独占模式下使用)
private transient Thread exclusiveOwnerThread;

1.3 Node节点结构

同步队列中的每个节点都是一个Node对象:

java 复制代码
static final class Node {
    // 节点等待模式
    static final Node SHARED = new Node();   // 共享模式
    static final Node EXCLUSIVE = null;      // 独占模式
    
    // 等待状态常量
    static final int CANCELLED = 1;   // 线程已取消等待
    static final int SIGNAL = -1;     // 当前节点释放锁后需要唤醒后继节点
    static final int CONDITION = -2;  // 在条件队列中等待
    static final int PROPAGATE = -3;  // 共享模式下需要传播唤醒
    
    // 节点状态(重要:-1 SIGNAL,1 CANCELLED,0 初始状态)
    volatile int waitStatus;
    
    // 双向链表指针
    volatile Node prev;   // 前驱节点
    volatile Node next;   // 后继节点
    
    // 该节点封装的线程
    volatile Thread thread;
    
    // 指向条件队列的下一个节点(Condition相关,本文暂不详述)
    Node nextWaiter;
}

1.4 同步队列结构图

重要特性

  1. 队列是FIFO(先进先出)的

  2. 头节点是一个哨兵节点,一般不关联具体的线程(或关联当前持有锁的线程)

  3. 每个节点封装一个等待的线程

  4. 节点状态SIGNAL表示:前驱节点释放锁后会唤醒我


二、非公平锁完整源码解析

2.1 加锁入口:lock()

java 复制代码
// ReentrantLock 构造方法
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

// 调用 lock() 时,实际执行的是 sync.lock()
public void lock() {
    sync.lock();
}

NonfairSync.lock()实现:

java 复制代码
final void lock() {
    // 【第一次插队机会】快速CAS尝试获取锁
    // 如果锁空闲(state=0),则直接抢锁成功
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  // 抢锁失败,进入完整获取流程
}

为什么叫非公平? 新来的线程可以不排队,直接尝试抢锁。如果抢成功了,队列中等待的线程只能继续等。

2.2 acquire:AQS模板方法

java 复制代码
public final void acquire(int arg) {
    // tryAcquire:尝试获取锁(包含重入逻辑)
    // addWaiter:获取失败,将当前线程封装成节点加入队列
    // acquireQueued:在队列中自旋等待
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();  // 如果等待过程中被中断过,补上中断标记
}

2.3 tryAcquire:尝试获取锁

AQS中的钩子方法:

java 复制代码
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

NonfairSync中的实现:

java 复制代码
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

核心逻辑 nonfairTryAcquire

java 复制代码
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    
    // 【情况1】锁空闲
    if (c == 0) {
        // 【第二次插队机会】CAS获取锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 【情况2】锁已被当前线程持有 → 重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // 溢出保护
            throw new Error("Maximum lock count exceeded");
        setState(nextc);  // 不需要CAS,因为只有当前线程才能执行到这里
        return true;
    }
    // 【情况3】锁被其他线程持有
    return false;
}

重入锁的关键 :同一个线程可以多次获取同一把锁,每次重入state加1,释放时需要释放相同次数。

2.4 addWaiter:入队操作

java 复制代码
private Node addWaiter(Node mode) {
    // 创建新节点,封装当前线程,mode=null表示独占模式
    Node node = new Node(Thread.currentThread(), mode);
    
    // 【快速尝试】如果队列已存在,尝试直接CAS追加到队尾
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 【兜底方案】队列不存在或CAS失败,进入自旋入队
    enq(node);
    return node;
}

2.5 enq:自旋入队(线程安全)

java 复制代码
private Node enq(final Node node) {
    for (;;) {  // 自旋,直到入队成功
        Node t = tail;
        if (t == null) {  // 队列尚未初始化
            // 创建哨兵节点作为头节点
            if (compareAndSetHead(new Node()))
                tail = head;  // 头尾指向同一个哨兵节点
        } else {
            // 标准入队操作
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;  // 返回前驱节点
            }
        }
    }
}

为什么要创建哨兵节点? 有了哨兵节点,队列永远不会为空,简化了唤醒逻辑------每次从头节点的后继开始唤醒即可。

2.6 acquireQueued:排队等待

这是最核心的等待逻辑:

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();  // 获取前驱节点
            
            // 【关键判断1】前驱是头节点 → 说明当前节点是队列中等待最久的
            if (p == head && tryAcquire(arg)) {
                // 获取锁成功,将当前节点设置为新头节点
                setHead(node);  // 头节点会清空thread引用
                p.next = null;  // help GC
                failed = false;
                return interrupted;  // 正常退出
            }
            
            // 【关键判断2】获取锁失败,判断是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;  // 记录中断状态
        }
    } finally {
        if (failed)
            cancelAcquire(node);  // 异常情况取消获取
    }
}

核心理解

  • 只有前驱是头节点的节点,才有资格尝试获取锁(保证FIFO)

  • 获取失败时,会阻塞自己,等待前驱节点唤醒

2.7 shouldParkAfterFailedAcquire:决定是否阻塞

java 复制代码
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    
    // 【情况1】前驱状态已经是SIGNAL
    if (ws == Node.SIGNAL)
        // 放心阻塞,前驱释放锁时会唤醒我
        return true;
    
    // 【情况2】前驱已取消等待
    if (ws > 0) {
        // 跳过所有取消的节点,向前找到第一个有效节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } 
    // 【情况3】前驱状态为0或PROPAGATE
    else {
        // 将前驱状态设置为SIGNAL
        // 注意:这里只是设置状态,返回false,外层会再尝试一次获取锁
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;  // 当前不需要阻塞,外层会再循环一次
}

为什么设置SIGNAL后返回false? 因为设置完成后,前驱节点可能刚好释放了锁,所以应该再给当前节点一次获取锁的机会。

2.8 parkAndCheckInterrupt:阻塞线程

java 复制代码
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);  // 阻塞当前线程
    return Thread.interrupted();  // 唤醒后返回中断状态并清除中断标记
}

LockSupport.park()会让线程进入WAITING状态,直到被unpark()唤醒。

2.9 加锁完整流程图

复制代码
                    ┌─────────────────────────────────────┐
                    │            lock() 调用               │
                    └─────────────────┬───────────────────┘
                                      ▼
                    ┌─────────────────────────────────────┐
                    │  快速CAS抢锁 (state=0→1)?            │
                    └─────────────────┬───────────────────┘
                           成功 │               │ 失败
                                ▼               ▼
                    ┌─────────────────┐   ┌─────────────────────────┐
                    │  获取锁成功      │   │  tryAcquire尝试获取锁    │
                    │  设置owner线程   │   │  (含重入逻辑)            │
                    └─────────────────┘   └───────────┬─────────────┘
                                                      ▼
                                           成功 │               │ 失败
                                                ▼               ▼
                                    ┌─────────────────┐   ┌─────────────────┐
                                    │  获取锁成功      │   │  addWaiter      │
                                    └─────────────────┘   │  入队           │
                                                          └────────┬────────┘
                                                                   ▼
                                                          ┌─────────────────┐
                                                          │  acquireQueued  │
                                                          │  自旋等待       │
                                                          └────────┬────────┘
                                                                   ▼
                                                          ┌─────────────────┐
                                                          │ 前驱是头节点?    │
                                                          └────────┬────────┘
                                                              是   │   否
                                                                   ▼
                                                          ┌─────────────────┐
                                                          │ tryAcquire成功?  │
                                                          └────────┬────────┘
                                                              是   │   否
                                                                   ▼
                                                          ┌─────────────────┐
                                                          │ 设置新头节点     │
                                                          │ 返回            │
                                                          └─────────────────┘
                                                                   │ 否
                                                                   ▼
                                                          ┌─────────────────┐
                                                          │ shouldPark...   │
                                                          │ 确保前驱SIGNAL  │
                                                          └────────┬────────┘
                                                                   ▼
                                                          ┌─────────────────┐
                                                          │ parkAndCheck... │
                                                          │ 阻塞线程        │
                                                          └────────┬────────┘
                                                                   │
                                    (唤醒后重新自旋) ◄──────────────┘

三、解锁流程完整解析

3.1 unlock入口

java 复制代码
public void unlock() {
    sync.release(1);
}

3.2 release:AQS释放模板

java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {  // 尝试释放锁
        Node h = head;
        // 头节点不为空且状态不为0(说明有需要唤醒的后继节点)
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  // 唤醒后继节点
        return true;
    }
    return false;
}

3.3 tryRelease:释放锁(重入减1)

java 复制代码
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 安全检查:只有持有锁的线程才能释放
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    
    boolean free = false;
    if (c == 0) {  // 完全释放(重入计数归零)
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);  // 更新state(不需要CAS,因为只有当前线程能执行这里)
    return free;
}

关键点 :重入锁的释放是分层的。每次unlock()只减少一次计数,只有计数归零时,锁才真正释放。

3.4 unparkSuccessor:唤醒后继线程

java 复制代码
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);  // 清除SIGNAL状态
    
    Node s = node.next;
    // 如果后继节点为空或已取消,从尾部向前找第一个有效节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 为什么要从后往前?因为并发环境下next指针可能不完整,但prev是可靠的
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);  // 唤醒线程
}

3.5 解锁流程图

复制代码
┌─────────────────────────────────────┐
│          unlock() 调用               │
└─────────────────┬───────────────────┘
                  ▼
┌─────────────────────────────────────┐
│  tryRelease: state减1                │
│  判断是否完全释放 (c == 0)?           │
└─────────────────┬───────────────────┘
         否 │               │ 是
            ▼               ▼
┌─────────────────┐   ┌─────────────────────────┐
│ 返回false        │   │  清空owner线程           │
│ 锁仍被持有       │   │  返回true                │
└─────────────────┘   └───────────┬─────────────┘
                                  ▼
                    ┌─────────────────────────────┐
                    │  head != null &&            │
                    │  head.waitStatus != 0?      │
                    └───────────┬─────────────────┘
                           是    │    否
                                ▼     ▼
                    ┌─────────────────┐   ┌─────────────────┐
                    │ unparkSuccessor │   │  直接返回        │
                    │ 唤醒后继节点     │   └─────────────────┘
                    └─────────────────┘

四、公平锁实现原理

4.1 公平锁与非公平锁的核心区别

公平锁与非公平锁的唯一区别在于:尝试获取锁时,是否检查队列中已有等待线程

特性 非公平锁 公平锁
新线程能否插队 能(两次插队机会) 不能
队列中等待线程的优先级 无特殊保护 等待最久的线程优先
吞吐量
可能产生饥饿
默认选择

4.2 公平锁的tryAcquire实现

java 复制代码
// FairSync 中的 tryAcquire 方法
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    
    if (c == 0) {
        // 【关键区别】公平锁要求:队列中没有等待时间更长的线程
        // hasQueuedPredecessors() 检查队列中是否有线程在等待
        // 如果队列为空,或者当前线程是队列中等待最久的,才允许尝试获取
        if (!hasQueuedPredecessors() && 
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入逻辑与非公平锁完全相同
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

4.3 hasQueuedPredecessors:检查队列中是否有等待线程

java 复制代码
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 条件为真的情况:队列不为空,且头节点的后继节点不是当前线程
    return h != t &&   // 队列中至少有2个节点(头节点 + 至少一个等待节点)
           ((s = h.next) == null ||  // 头节点的后继为空(极端情况)
            s.thread != Thread.currentThread());  // 后继节点的线程不是当前线程
}

这个方法的核心逻辑

  • 如果队列为空 → 返回false(没有等待者)

  • 如果队列只有哨兵节点 → 返回false(没有实际等待的线程)

  • 如果队列中第一个等待线程是当前线程 → 返回false(重入场景,允许获取)

  • 否则 → 返回true(有其他线程在队列中等待,当前线程不能插队)

4.4 公平锁加锁流程对比

复制代码
非公平锁:
  线程到来 → 尝试CAS抢锁 → 失败 → tryAcquire再尝试 → 失败 → 入队等待

公平锁:
  线程到来 → tryAcquire检查队列 → 有等待者则直接入队 → 入队等待
  (没有lock开头的快速CAS抢锁)

注意 :公平锁的lock()方法中没有开头的CAS抢锁:

java 复制代码
// FairSync.lock() 直接调用 acquire,没有快速尝试
final void lock() {
    acquire(1);
}

4.5 非公平的两次插队机会(回顾)

非公平锁之所以"非公平",在于新来的线程有两次插队机会:

  1. 第一次插队lock()方法开头的compareAndSetState(0, 1)

  2. 第二次插队tryAcquire()中的compareAndSetState(0, acquires)

java 复制代码
// NonfairSync.lock()
final void lock() {
    if (compareAndSetState(0, 1))  // ← 插队机会1
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

// nonfairTryAcquire()
if (c == 0) {
    if (compareAndSetState(0, acquires))  // ← 插队机会2
        // ...
}

五、完整加锁流程对比总结

5.1 非公平锁完整流程

复制代码
线程调用 lock()
    │
    ▼
【插队1】CAS(state=0→1) 成功? ──是──▶ 设置owner → 返回
    │ 否
    ▼
tryAcquire() → nonfairTryAcquire()
    │
    ├── state=0? ──是──▶ 【插队2】CAS抢锁
    │                        │
    │                        ├── 成功 → 设置owner → 返回
    │                        └── 失败 → 继续
    │
    ├── owner=当前线程? ──是──▶ state+1 → 返回成功
    │
    └── 其他情况 → 返回失败
    │
    ▼
addWaiter() → 创建节点加入队列
    │
    ▼
acquireQueued() → 自旋等待
    │
    ├── 前驱是头节点且tryAcquire成功? ──是──▶ 设为新头节点 → 返回
    │
    └── 否则 → shouldPark... → parkAndCheck... → 阻塞

5.2 公平锁完整流程

复制代码
线程调用 lock() → acquire(1)
    │
    ▼
tryAcquire() (FairSync版本)
    │
    ├── state=0 且 队列中无等待者? ──是──▶ CAS抢锁成功 → 设置owner → 返回
    │
    ├── owner=当前线程? ──是──▶ state+1 → 返回成功
    │
    └── 其他情况 → 返回失败
    │
    ▼
addWaiter() → 创建节点加入队列
    │
    ▼
acquireQueued() → 自旋等待 (与非公平锁相同)

5.3 关键区别总结表

比较项 非公平锁 公平锁
lock()开头快速CAS ✅ 有 ❌ 无
tryAcquire中state=0时 直接CAS抢锁(不管队列) 检查队列无等待者才CAS
插队次数 2次 0次
吞吐量
饥饿风险

六、常见问题深度解答

Q1:为什么AQS队列的头节点是空节点(哨兵)?

:这是一个设计优化,主要原因:

  1. 分离"当前持有锁的线程"和"队列管理"的职责

  2. 释放锁时,直接从头节点的后继节点开始唤醒,逻辑统一

  3. 避免空指针判断,简化代码

Q2:为什么unparkSuccessor要从尾部向前遍历?

:因为并发入队时,next指针可能不是最新的:

  • enq()方法中,先设置prev和CAStail,最后才设置next

  • 如果刚设置完tail但还没设置next,从头向后遍历会丢失新节点

  • prev指针一旦设置就不会改变,所以从尾部向前遍历是可靠的

Q3:非公平锁为什么性能更好?

:因为减少了线程挂起/唤醒的次数:

  • 公平锁:线程释放锁后,必须唤醒队列中的下一个线程

  • 非公平锁:新来的线程可能直接抢到锁,避免了唤醒开销

  • 在高并发场景下,减少上下文切换能显著提升吞吐量

Q4:如何实现超时获取锁?

:AQS提供了tryAcquireNanos方法,核心逻辑是:

java

复制代码
private boolean doAcquireNanos(int arg, long nanosTimeout) {
    // ... 
    for (;;) {
        // 尝试获取锁...
        nanosTimeout = deadline - System.nanoTime();
        if (nanosTimeout <= 0L)
            return false;  // 超时返回
        if (shouldParkAfterFailedAcquire(p, node) &&
            nanosTimeout > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);  // 限时等待
        // ...
    }
}

Q5:AQS如何支持共享模式(如Semaphore)?

:共享模式与独占模式的区别:

  • 独占模式:state表示锁是否被占用(0/1或重入次数)

  • 共享模式:state表示可用资源数量(如Semaphore的许可数)

  • 共享模式的节点释放后,会传播唤醒后面的共享节点(PROPAGATE状态)


七、总结

7.1 核心知识点回顾

组件 作用
state 同步状态,ReentrantLock中表示重入次数
exclusiveOwnerThread 当前持有锁的线程
head/tail 同步队列的头尾指针
Node 队列节点,封装等待线程和前驱后继关系
waitStatus 节点状态,SIGNAL表示需要唤醒后继
LockSupport 线程阻塞/唤醒的工具类

7.2 关键设计思想

  1. CAS + 自旋:无锁化实现线程安全的入队操作

  2. 模板方法模式:AQS定义骨架,子类实现钩子方法

  3. CLH队列变种:双向链表便于取消和唤醒

  4. 哨兵节点:简化边界条件处理

  5. 可重入设计:state计数 + owner线程判断

7.3 一句话总结

AQS通过一个volatile的state变量表示同步状态,通过一个FIFO的双向CLH队列管理等待线程,利用CAS+自旋实现无锁入队,通过LockSupport实现线程的阻塞与唤醒。非公平锁允许两次插队提升性能,公平锁通过hasQueuedPredecessors检查保证先来后到。

相关推荐
兰令水12 小时前
topcode【随机算法题】【2026.5.22打卡-java版本】
java·算法·leetcode
Dicky-_-zhang12 小时前
敏感数据加密存储实战
java·jvm
念何架构之路12 小时前
Go依赖管理
开发语言·后端·golang
liudanzhengxi12 小时前
CUDA转OpenCL:跨平台内核迁移实战
开发语言
吃好睡好便好12 小时前
用if…elseif…end语句输出成绩等级
开发语言·前端·javascript·数据库·学习·matlab·信息可视化
努力努力再努力wz12 小时前
【Redis入门系列】:Redis 内部编码机制与 String 深度解析:SDS 底层实现、三种编码与核心命令详解
c语言·开发语言·数据结构·数据库·c++·redis·缓存
必胜刻12 小时前
Go 调用Coze工作流实现 AI 游戏生成
开发语言·ai·golang·gin
夕除12 小时前
spring boot 12
java·开发语言·python
Brilliantwxx12 小时前
【C++】 认识STL set与map(基础接口+题目OJ运用)
开发语言·数据结构·c++·笔记·算法