ReentrantLock 锁机制原理

139 阅读9分钟

ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中最核心的显式锁实现,基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器)  构建,支持可重入、公平 / 非公平选择、可中断、超时获取等特性,其设计核心是通过 AQS 管理锁的竞争状态和等待队列,同时通过自身逻辑实现可重入等增强功能。

要理解 ReentrantLock 的原理,需先明确其核心依赖(AQS),再拆解锁的获取、释放、可重入、公平 / 非公平等关键机制。

一、核心依赖:AQS 基础铺垫

ReentrantLock 的底层逻辑完全依赖 AQS,AQS 是 Java 并发包的 “同步骨架”,其核心作用是:

  1. 维护一个 volatile 修饰的状态变量 state:表示锁的占用状态(0 = 无锁,>0 = 有锁,支持可重入)。
  2. 维护一个 FIFO 双向等待队列:存放竞争锁失败的线程(阻塞状态)。
  3. 提供 CAS 操作:原子修改 state 和队列节点,保证并发安全。
  4. 提供 模板方法:由子类(ReentrantLock 的内部同步器)实现 tryAcquire()(获取锁逻辑)、tryRelease()(释放锁逻辑)等核心方法,AQS 负责队列管理、线程阻塞 / 唤醒等通用逻辑。

ReentrantLock 内部通过两个内部类实现 AQS 的模板方法:

  • NonfairSync:非公平锁(默认),性能更优。
  • FairSync:公平锁,按线程等待顺序分配锁。

两者的核心差异仅在于 锁获取时的竞争逻辑,其他机制(可重入、释放锁、队列管理)完全一致。

二、ReentrantLock 核心机制拆解

1. 锁的状态管理:state 变量的作用

AQS 的 state 是 ReentrantLock 状态的核心载体,含义如下:

  • state = 0:锁未被任何线程持有(无锁状态)。
  • state > 0:锁被某线程持有,state 的值等于该线程的 重入次数(支持可重入)。

例如:线程 A 第一次获取锁时,state 从 0 变为 1;若线程 A 再次重入(如递归调用加锁方法),state 变为 2;每次释放锁时,state 减 1,直到 state = 0 时,锁才真正释放。

2. 非公平锁(默认)的获取与释放流程

非公平锁的核心是:新竞争锁的线程可 “插队”,无需排队(优先尝试 CAS 获取锁,失败后再入队),这是其性能优于公平锁的关键。

(1)锁获取流程(lock() 方法)

// ReentrantLock 的 lock() 方法(默认调用 NonfairSync 的 lock())
public void lock() {
    sync.lock(); // sync 是 NonfairSync 或 FairSync 实例
}

// NonfairSync 的 lock() 实现
final void lock() {
    // 第一步:尝试 CAS 抢锁(插队逻辑):state 从 0 改为 1
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread()); // 标记当前线程为锁持有者
    } else {
        // 抢锁失败,调用 AQS 的 acquire() 模板方法(通用逻辑)
        acquire(1); 
    }
}

// AQS 的 acquire() 模板方法(通用逻辑)
public final void acquire(int arg) {
    // 第二步:调用 NonfairSync 的 tryAcquire() 再次尝试获取锁(可重入逻辑)
    if (!tryAcquire(arg) && 
        // 第三步:尝试失败,将当前线程封装为节点加入等待队列,然后阻塞
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt(); // 若线程在等待中被中断,标记中断状态
    }
}

关键步骤拆解:

  1. 插队抢锁:新线程先通过 CAS(0,1) 直接尝试获取锁(不排队),成功则直接持有锁。

  2. tryAcquire () 逻辑(可重入 + 再次抢锁) :若插队失败(state≠0),调用 tryAcquire() 处理两种情况:

    • 情况 1:当前线程是锁的持有者(可重入):state += arg(arg=1),返回成功。
    • 情况 2:当前线程不是持有者:返回失败,进入队列。
// NonfairSync 的 tryAcquire() 实现
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// ReentrantLock 父类的 nonfairTryAcquire()(核心逻辑)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { // 锁未被持有,再次尝试 CAS 抢锁(仍允许插队)
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) { // 当前线程是持有者(可重入)
        int nextc = c + acquires;
        if (nextc < 0) // 防止重入次数溢出(int 最大值)
            throw new Error("Maximum lock count exceeded");
        setState(nextc); // 重入次数+1(无需 CAS,因为只有持有者能执行)
        return true;
    }
    return false; // 其他线程,抢锁失败
}
  1. 入队阻塞tryAcquire() 失败后,AQS 会调用 addWaiter() 将当前线程封装为 排他节点(Node.EXCLUSIVE)加入等待队列尾部,再通过 acquireQueued() 让线程阻塞(通过 LockSupport.park() 暂停线程),等待被唤醒。

(2)锁释放流程(unlock() 方法)

ReentrantLock 必须手动调用 unlock() 释放锁(需在 finally 中执行,避免锁泄漏),释放逻辑核心是 递减重入次数,直到 state=0 时真正释放锁(唤醒队列头节点)

// ReentrantLock 的 unlock() 方法
public void unlock() {
    sync.release(1); // 调用 AQS 的 release() 模板方法
}

// AQS 的 release() 模板方法(通用逻辑)
public final boolean release(int arg) {
    // 第一步:调用 ReentrantLock 的 tryRelease() 释放锁
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h); // 第二步:唤醒等待队列的头节点(下一个竞争锁的线程)
        }
        return true;
    }
    return false;
}

// ReentrantLock 的 tryRelease() 实现(核心释放逻辑)
protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 重入次数-1
    if (Thread.currentThread() != getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException(); // 非持有者释放锁,抛异常
    }
    boolean free = false;
    if (c == 0) { // 重入次数减为 0,锁真正释放
        free = true;
        setExclusiveOwnerThread(null); // 清除锁持有者标记
    }
    setState(c); // 更新 state(c≠0 时仅递减重入次数,未释放锁)
    return free;
}

关键说明:

  • 释放锁时,只有锁的持有者才能执行(否则抛异常)。
  • 若重入次数未减到 0(c≠0),仅更新 state,锁仍被当前线程持有,不会唤醒队列。
  • 只有 c=0 时,才会清除持有者标记,并调用 unparkSuccessor() 唤醒队列头节点的下一个有效线程(被唤醒的线程会再次尝试获取锁)。

3. 公平锁的核心差异:排队抢锁,禁止插队

公平锁与非公平锁的唯一区别的是 tryAcquire() 方法:公平锁会先检查等待队列是否有线程在排队,若有则当前线程必须入队,禁止插队;若无队列才尝试 CAS 获取锁。

// 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;
}

// AQS 的 hasQueuedPredecessors():判断队列是否有线程在等待(公平性核心)
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    // 队列不为空(h≠t),且队列头的下一个节点(s)不是当前线程 → 有线程排队
    return h != t && 
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

公平锁的优缺点:

  • 优点:保证线程等待的公平性(先到先得),避免 “饥饿”(某线程长期抢不到锁)。
  • 缺点:性能略低(需频繁检查队列,减少了 “插队” 的高效场景,增加线程切换开销)。

4. 可重入机制的底层实现

ReentrantLock 的可重入性(同一线程可多次获取同一锁),依赖两个核心设计:

  1. AQS 的 state 变量记录重入次数:线程第一次获取锁时 state=1,每次重入 state+1,释放时 state-1,直到 state=0 才释放锁。
  2. 锁持有者标记:AQS 维护 exclusiveOwnerThread 变量(记录当前锁的持有者线程),tryAcquire() 时会检查当前线程是否为持有者,若是则直接递增 state,无需竞争。

核心逻辑在 nonfairTryAcquire() 和 fairTryAcquire() 中:当 state≠0 时,判断当前线程是否为 exclusiveOwnerThread,若是则执行 state += acquires,实现重入。

5. 条件变量(Condition)机制

ReentrantLock 支持通过 newCondition() 创建多个条件变量(Condition),实现更精细的线程协作(如生产者 - 消费者模型中,可区分 “空队列等待” 和 “满队列等待”),而 synchronized 仅支持一个等待队列(wait()/notify())。

Condition 的核心原理:

  • 每个 Condition 对应一个 独立的等待队列(与 AQS 的主等待队列分离)。
  • 调用 condition.await() 时:当前线程会释放锁,从 AQS 主队列移除,加入 Condition 的等待队列,然后阻塞。
  • 调用 condition.signal() 时:从 Condition 等待队列头部唤醒一个线程,将其转移到 AQS 主队列,参与锁竞争。

关键流程示例:

ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 条件变量:队列非空
Condition notFull = lock.newCondition();  // 条件变量:队列未满

// 消费者线程(等待队列非空)
lock.lock();
try {
    while (queue.isEmpty()) {
        notEmpty.await(); // 释放锁,加入 notEmpty 的等待队列,阻塞
    }
    // 消费数据
    notFull.signal(); // 唤醒 notFull 等待队列中的生产者线程
} finally {
    lock.unlock();
}

// 生产者线程(等待队列未满)
lock.lock();
try {
    while (queue.isFull()) {
        notFull.await(); // 释放锁,加入 notFull 的等待队列,阻塞
    }
    // 生产数据
    notEmpty.signal(); // 唤醒 notEmpty 等待队列中的消费者线程
} finally {
    lock.unlock();
}

底层逻辑:

  • Condition 是 AQS 的内部类,其等待队列由 Node 节点组成(与 AQS 主队列节点结构一致)。
  • await() 方法会调用 release() 释放锁,再将线程加入 Condition 队列,最后通过 LockSupport.park() 阻塞。
  • signal() 方法会将 Condition 队列的头节点转移到 AQS 主队列,等待被 unpark() 唤醒后竞争锁。

6. 可中断与超时获取锁

ReentrantLock 支持两种增强的锁获取方式,底层依赖 AQS 的中断机制和超时逻辑:

(1)可中断获取锁(lockInterruptibly()

线程在等待锁的过程中,可被 interrupt() 中断,避免无限期等待。

  • 原理:AQS 的 acquireInterruptibly() 模板方法会检查线程的中断状态,若被中断则抛出 InterruptedException,并退出等待。

(2)超时获取锁(tryLock(long timeout, TimeUnit unit)

线程尝试获取锁,若超过指定时间仍未获取则返回 false,避免死锁风险。

  • 原理:AQS 的 tryAcquireNanos() 模板方法会计算超时时间,在等待过程中定期检查是否超时,超时则返回失败,线程无需继续阻塞。

三、ReentrantLock 与 synchronized 的核心差异

特性ReentrantLocksynchronized
底层实现基于 AQS(Java 代码实现)基于 JVM 原生 monitor 锁(C++ 实现)
锁获取 / 释放手动调用 lock()/unlock()(需 finally)自动加锁 / 释放(进入 / 退出同步块)
公平性支持公平 / 非公平(构造函数指定)仅非公平锁
可中断性支持(lockInterruptibly()不支持(线程等待时无法中断)
超时获取支持(tryLock(timeout)不支持
条件变量支持多个 Condition(精细协作)仅支持一个等待队列(wait()/notify()
锁状态查询支持(isLocked()getHoldCount()不支持

四、核心总结

ReentrantLock 的锁机制可概括为:

  1. 核心骨架:基于 AQS 实现,通过 state 变量管理锁状态,通过 FIFO 队列管理等待线程。
  2. 锁竞争逻辑:非公平锁(默认)支持 “插队” 抢锁,公平锁按队列顺序抢锁,平衡性能与公平性。
  3. 可重入实现:通过 state 记录重入次数 + exclusiveOwnerThread 标记持有者,同一线程可多次获取锁。
  4. 增强特性:依赖 AQS 的中断、超时机制,以及 Condition 条件队列,实现可中断、超时获取、多条件协作。
  5. 手动管理:需手动释放锁(finally 中),避免锁泄漏,灵活性更高但需注意使用规范。

适用场景:复杂并发场景(如需要中断、超时、公平性、多条件协作),简单场景下 synchronized 更简洁(JVM 优化充分,性能接近 ReentrantLock)。