跳转到正文
Siolin'Log
返回

Java ReentrantLock:重入、公平性与条件队列

概述

ReentrantLock 与 AQS 的实现关系

ReentrantLock 是 Java 并发包中基于 AQS 实现的可重入独占锁

可以先用一句话理解:它和 synchronized 都能实现互斥,但 ReentrantLock 提供了可中断、超时、公平锁和多个条件队列等更强的控制能力。

各个获取 API 的通用语义可先参考 Java Lock;本文重点解释 ReentrantLock 自身的重入、公平策略和实现流程。

可重入机制

下面用简化伪代码表示当前 JDK 实现中的核心判断。具体私有方法名会随 JDK 版本演进,不应把某个版本的 nonfairTryAcquire() 当成稳定 API:

boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (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;
}

这让一个加锁方法调用另一个加锁方法时不会被自己卡住。

常用 API

lock()

等待直到获取锁:

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

等待过程中不能通过普通中断立即取消。

lockInterruptibly()

等待锁时允许被中断:

try {
    lock.lockInterruptibly();
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

适合可能长时间等待、需要支持任务取消的场景。

tryLock()

立即尝试获取锁,不等待:

if (lock.tryLock()) {
    try {
        // 获取成功
    } finally {
        lock.unlock();
    }
} else {
    // 获取失败,执行降级逻辑
}

超时获取

try {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    } else {
        throw new RuntimeException("获取锁超时");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

这是它相对于 synchronized 的重要优势:线程不必无限等待

公平锁和非公平锁

ReentrantLock 默认是非公平锁:

new ReentrantLock();      // 非公平
new ReentrantLock(true);  // 公平

非公平锁

锁释放时,新来的线程可以直接尝试抢锁,不必严格排队。

优点:

缺点:

公平锁

在存在竞争时,公平锁倾向于把锁交给等待时间最长的线程,但它不保证线程调度本身公平,也不代表严格的业务执行顺序。

优点:

缺点:

无参 tryLock() 即使面对公平锁也可以在锁可用时插队。选择公平模式前应先确认业务确实需要接近先到先得,并接受吞吐量下降;不能仅凭“公平听起来更正确”来选择。

Condition 条件队列

Condition 可以理解成比 Object.wait()notify() 更灵活的线程等待机制。一把 ReentrantLock 可以关联多个条件队列,完整流程见 Java Condition

Condition condition = lock.newCondition();

等待:

lock.lock();
try {
    while (!conditionSatisfied()) {
        condition.await();
    }

    // 条件满足,继续执行
} finally {
    lock.unlock();
}

唤醒:

lock.lock();
try {
    updateState();
    condition.signal();
} finally {
    lock.unlock();
}

必须先持有对应的锁,才能调用:

await()

线程执行 await() 时:

  1. 加入 Condition 等待队列
  2. 完全释放当前持有的锁
  3. 进入等待状态
  4. signal() 唤醒后,转移到锁的同步队列
  5. 重新竞争锁
  6. 获取锁后,从 await() 返回

注意:signal() 并不代表被唤醒线程会立即执行。它必须等当前线程释放锁后,再重新竞争锁。

必须使用 while

错误写法:

if (queue.isEmpty()) {
    notEmpty.await();
}

正确写法:

while (queue.isEmpty()) {
    notEmpty.await();
}

因为线程被唤醒后:

所以每次恢复执行后都要重新检查条件。

非公平锁获取

下面的流程图描述默认非公平模式的主要路径;它省略了取消、超时、中断和队列清理等实现细节。

flowchart TD
    A["调用 lock()"] --> B{"state == 0?"}

    B -->|"是"| C["CAS:state 从 0 改为 1"]
    C --> D{"CAS 成功?"}
    D -->|"成功"| E["记录当前线程为 Owner"]
    D -->|"失败"| F["进入 AQS acquire 流程"]

    B -->|"否"| G{"Owner 是当前线程?"}
    G -->|"是"| H["state = state + 1<br/>完成重入"]
    G -->|"否"| F

    F --> I["加入 AQS Sync Queue 尾部"]
    I --> J["检查前驱节点和获取资格"]
    J --> K{"获取锁成功?"}

    K -->|"成功"| E
    K -->|"失败"| L["LockSupport.park() 挂起"]
    L -->|"被唤醒"| J


上一篇
Java 内存模型:happens-before 与可见性
下一篇
Java synchronized:用法、Monitor 与锁竞争