ReentrantLock详解

ReentrantLock 是 Java 并发包 java.util.concurrent.locks (JUC) 下一个非常重要的类,它实现了 Lock 接口,提供了一种比 synchronized 关键字更强大、更灵活的线程同步机制。

1. 什么是 ReentrantLock?

ReentrantLock,中文意为 “可重入锁”。顾名思义,它最大的特点就是 可重入性。

核心概念:可重入性 (Reentrancy)

可重入性指的是,同一个线程可以重复地获取它已经持有的锁。当一个线程请求一个它已经持有的锁时,该请求会立即成功。

为了实现这一点,ReentrantLock 内部维护了一个计数器。当线程第一次获取锁时,计数器变为 1。该线程每再次(重入)获取一次锁,计数器就加 1。相应地,每释放一次锁,计数器就减 1。直到计数器变为 0 时,该锁才被完全释放,其他线程才有机会获取它。

示例代码:

class ReentrantExample {

private final ReentrantLock lock = new ReentrantLock();

public void outerMethod() {

lock.lock(); // 第一次获取锁,计数器变为 1

try {

System.out.println("进入 outerMethod,当前线程:" + Thread.currentThread().getName());

innerMethod(); // 在持有锁的情况下调用另一个需要相同锁的方法

} finally {

lock.unlock(); // 最后一次释放锁,计数器变为 0

}

}

public void innerMethod() {

lock.lock(); // 重入锁,计数器变为 2

try {

System.out.println("进入 innerMethod,当前线程:" + Thread.currentThread().getName());

} finally {

lock.unlock(); // 释放锁,计数器变为 1

}

}

public static void main(String[] args) {

ReentrantExample example = new ReentrantExample();

new Thread(example::outerMethod).start();

}

}

在这个例子中,如果 ReentrantLock 不是可重入的,那么在 outerMethod 中调用 innerMethod 时,线程会因为无法获取已经被自己持有的锁而导致死锁。synchronized 关键字同样也具有可重入性。

2. ReentrantLock vs. synchronized

ReentrantLock 和 synchronized 是 Java 中最常用的两种同步机制。它们既有相似之处,也有显著的不同。

特性synchronizedReentrantLock本质Java 关键字,由 JVM 实现。Java 类库中的一个 API (java.util.concurrent.locks.Lock)。锁的释放自动释放。代码块执行完毕或抛出异常后,JVM 会自动释放锁。手动释放。必须在 finally 块中调用 unlock() 方法来释放锁,否则可能导致死锁。锁的获取阻塞式,无法中断。提供多种获取方式,如可中断 (lockInterruptibly)、可超时 (tryLock)。公平性非公平锁。默认非公平,可配置为公平锁。功能特性功能相对简单。功能丰富,提供了 synchronized 不具备的高级功能。性能在 JDK 1.6 之后,synchronized 经过了大量优化(如锁升级),性能与 ReentrantLock 已相差无几。在高竞争环境下,通过合理使用其高级特性,可能获得更好的性能。对象绑定与对象实例或类绑定。可独立于对象存在。条件变量通过 Object 类的 wait(), notify(), notifyAll() 实现。通过 Condition 对象实现,可以更精确地控制,支持多个条件队列。

3. ReentrantLock 的核心特性与用法

ReentrantLock 的强大之处在于它提供的丰富功能。

3.1. 标准用法 (必须使用 finally)

这是使用 ReentrantLock 的标准范式,必须将 unlock() 调用放在 finally 块中,以确保即使在业务代码发生异常时锁也能被正确释放。

import java.util.concurrent.locks.ReentrantLock;

class Counter {

private final ReentrantLock lock = new ReentrantLock();

private int count = 0;

public void increment() {

lock.lock(); // 获取锁

try {

// 临界区(受保护的代码)

count++;

} finally {

lock.unlock(); // 在 finally 块中释放锁

}

}

}

3.2. 公平锁 (Fair Lock) 与非公平锁 (Non-fair Lock)

非公平锁 (默认): 当锁被释放时,任何正在等待的线程都有机会获取锁,新来的线程也可能 “插队” 成功。这种方式能提供更高的吞吐量(性能更好)。公平锁: 严格按照线程请求锁的等待时间顺序来分配锁(类似排队)。它能防止线程饥饿,但上下文切换更频繁,吞吐量通常较低。

如何创建公平锁:

// 创建一个非公平锁(默认)

ReentrantLock nonFairLock = new ReentrantLock();

// 创建一个公平锁

// 构造函数传入 true

ReentrantLock fairLock = new ReentrantLock(true);

3.3. 可中断的锁获取 (lockInterruptibly())

使用 synchronized 时,如果一个线程在等待锁,它会一直阻塞,无法响应中断。而 ReentrantLock 的 lockInterruptibly() 方法允许在等待锁的过程中响应中断。

void someMethod() {

try {

// 尝试获取锁,如果线程在等待时被中断,会抛出 InterruptedException

lock.lockInterruptibly();

try {

// ... 业务逻辑 ...

} finally {

lock.unlock();

}

} catch (InterruptedException e) {

// 线程在等待锁时被中断,执行中断处理逻辑

System.out.println("线程被中断,不再等待锁。");

Thread.currentThread().interrupt(); // 重新设置中断状态

}

}

3.4. 可定时的锁获取 (tryLock())

tryLock() 方法尝试立即获取锁,如果锁不可用,它不会等待,而是直接返回 false。这可以用来避免死锁或执行一些替代逻辑。

它的重载版本 tryLock(long timeout, TimeUnit unit) 允许线程在指定时间内等待,如果在超时前还未获取到锁,则返回 false。

void timedLockMethod() {

try {

// 在 2 秒内尝试获取锁

if (lock.tryLock(2, TimeUnit.SECONDS)) {

try {

System.out.println("成功获取到锁!");

// ... 业务逻辑 ...

} finally {

lock.unlock();

}

} else {

System.out.println("在规定时间内未能获取到锁。");

// ... 执行替代逻辑 ...

}

} catch (InterruptedException e) {

// 等待过程中被中断

Thread.currentThread().interrupt();

}

}

3.5. 条件变量 (Condition)

ReentrantLock 可以通过 newCondition() 方法创建一个或多个 Condition 对象。Condition 接口提供了类似 Object 监视器方法 (wait, notify, notifyAll) 的功能,但更强大、更灵活。

condition.await(): 类似于 object.wait(),使当前线程等待,并释放锁。condition.signal(): 类似于 object.notify(),唤醒一个在 await() 的线程。condition.signalAll(): 类似于 object.notifyAll(),唤醒所有在 await() 的线程。

一个 ReentrantLock 可以绑定多个 Condition 对象,这使得实现复杂的同步场景(如著名的生产者-消费者模型中的“有界缓冲区”)变得非常简单和清晰。

生产者-消费者示例:

class BoundedBuffer {

final ReentrantLock lock = new ReentrantLock();

final Condition notFull = lock.newCondition(); // 条件:缓冲区未满

final Condition notEmpty = lock.newCondition(); // 条件:缓冲区非空

final Object[] items = new Object[100];

int putptr, takeptr, count;

public void put(Object x) throws InterruptedException {

lock.lock();

try {

while (count == items.length) {

notFull.await(); // 缓冲区已满,等待 "notFull" 条件

}

items[putptr] = x;

if (++putptr == items.length) putptr = 0;

++count;

notEmpty.signal(); // 缓冲区不再为空,通知消费者

} finally {

lock.unlock();

}

}

public Object take() throws InterruptedException {

lock.lock();

try {

while (count == 0) {

notEmpty.await(); // 缓冲区为空,等待 "notEmpty" 条件

}

Object x = items[takeptr];

if (++takeptr == items.length) takeptr = 0;

--count;

notFull.signal(); // 缓冲区不再为满,通知生产者

return x;

} finally {

lock.unlock();

}

}

}

在这个例子中,生产者和消费者分别在不同的条件上等待,互不干扰,控制更加精确。如果使用 synchronized,你只有一个等待队列,无法区分是该唤醒生产者还是消费者。

4. 底层实现简介 (AQS)

ReentrantLock (以及 JUC 中的许多其他同步器) 是基于 AbstractQueuedSynchronizer (AQS) 框架实现的。AQS 是一个用来构建锁和同步器的核心框架。

它内部维护一个 state 变量(一个 int 或 long),用来表示同步状态(在 ReentrantLock 中,它就是锁的重入计数器)。它还维护一个 CLH (Craig, Landin, and Hagersten) 虚拟双向队列,用于管理所有等待获取锁的线程。当一个线程尝试获取锁失败后,它会被封装成一个节点加入到这个等待队列的尾部,并进入休眠状态。当持有锁的线程释放锁时,AQS 会唤醒队列头部的线程,让它尝试再次获取锁。

ReentrantLock 的公平与非公平版本就是通过 AQS 来实现的。公平锁严格遵循队列的 FIFO 顺序,而非公平锁则允许新线程 “插队”。

5. 总结与选择建议

何时选择 ReentrantLock?

尽管 ReentrantLock 功能强大,但 synchronized 依然是许多场景下的首选,因为它更简单、不易出错(自动释放锁)。

建议:

首选 synchronized: 在同步需求不复杂的情况下,优先使用 synchronized。代码更简洁,由 JVM 保证锁的正确释放,降低了出错风险。需要高级功能时使用 ReentrantLock: 当你需要以下一个或多个 synchronized 不具备的功能时,就应该选择 ReentrantLock:

需要中断一个正在等待锁的线程。需要定时或非阻塞地尝试获取锁。需要实现公平锁以避免线程饥饿。需要在一个锁上创建多个条件队列(Condition),以实现更复杂的线程间通信。

总而言之,ReentrantLock 是 synchronized 的一个功能更丰富的替代品,它为 Java 程序员提供了更高层次的对锁的控制。掌握它对于编写高性能、高灵活性的并发程序至关重要。