一、ReentrantLock 基础概述
ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中提供的一种可重入的互斥锁实现,它实现了 Lock 接口,提供了比 synchronized 更灵活、更强大的同步机制。
1.1 ReentrantLock 的核心特性
- 可重入性:支持同一个线程多次获取锁
- 可中断性:支持中断等待锁的线程
- 公平性选择:支持公平锁和非公平锁两种模式
- 条件变量支持:通过
Condition接口实现更灵活的线程间通信 - 显式获取和释放:需要手动调用
lock()和unlock()方法
1.2 ReentrantLock 的使用场景
- 需要可中断锁的场景
- 需要公平锁的场景
- 需要多个条件变量的场景
- 需要尝试获取锁(tryLock)的场景
- 需要超时获取锁的场景
二、ReentrantLock 的基本使用
2.1 创建 ReentrantLock 实例
// 创建非公平锁(默认)
ReentrantLock lock = new ReentrantLock();
// 创建公平锁
ReentrantLock fairLock = new ReentrantLock(true);2.2 基本的锁获取和释放
ReentrantLock lock = new ReentrantLock();
// 获取锁
lock.lock();
try {
// 同步代码块
// 访问共享资源
} finally {
// 释放锁,必须在finally块中执行
lock.unlock();
}注意:释放锁的操作必须放在 finally 块中,以确保即使发生异常,锁也能被正确释放。
三、ReentrantLock 的高级功能
3.1 可中断的锁获取
lockInterruptibly() 方法允许线程在等待锁的过程中被中断:
ReentrantLock lock = new ReentrantLock();
Thread thread = new Thread(() -> {
try {
// 可被中断的锁获取
lock.lockInterruptibly();
try {
// 同步代码块
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断
Thread.currentThread().interrupt();
}
});
thread.start();
// 中断线程
thread.interrupt();3.2 尝试获取锁
tryLock() 方法尝试获取锁,如果获取成功返回 true,否则返回 false,不会阻塞:
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// 同步代码块
} finally {
lock.unlock();
}
} else {
// 获取锁失败的处理逻辑
}3.3 带超时的锁获取
tryLock(long timeout, TimeUnit unit) 方法尝试在指定时间内获取锁:
ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 同步代码块
} finally {
lock.unlock();
}
} else {
// 超时处理逻辑
}
} catch (InterruptedException e) {
// 处理中断
Thread.currentThread().interrupt();
}3.4 条件变量
ReentrantLock 可以通过 newCondition() 方法创建多个条件变量,用于线程间通信:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待条件满足
lock.lock();
try {
while (!conditionSatisfied()) {
// 当前线程释放锁并等待
condition.await();
}
// 条件满足后的处理逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
// 通知等待的线程条件已满足
lock.lock();
try {
// 更新条件状态
updateCondition();
// 唤醒一个等待的线程
condition.signal();
// 或者唤醒所有等待的线程
// condition.signalAll();
} finally {
lock.unlock();
}四、ReentrantLock 的实现原理
4.1 基于 AQS 的实现
ReentrantLock 基于 Java 并发包中的 AbstractQueuedSynchronizer (AQS) 框架实现,主要包含以下核心组件:
- 同步器(Sync):
ReentrantLock的内部抽象类,继承自 AQS,实现了锁的基本操作 - 非公平同步器(NonfairSync):实现非公平锁的逻辑
- 公平同步器(FairSync):实现公平锁的逻辑
4.2 锁的获取机制
- 非公平锁:线程尝试获取锁时,首先会尝试直接获取,如果成功则获取锁;否则,才会进入等待队列。
- 公平锁:线程必须按照请求锁的顺序来获取锁,先到先得。
4.3 可重入性的实现
ReentrantLock 通过在 AQS 的 state 变量中记录持有锁的线程获取锁的次数来实现可重入性:
- 当线程第一次获取锁时,将
state设为 1 - 当同一线程再次获取锁时,将
state加 1 - 当线程释放锁时,将
state减 1 - 当
state减为 0 时,表示锁被完全释放
4.4 等待队列管理
ReentrantLock 使用 AQS 提供的 CLH (Craig, Landin, and Hagersten) 队列来管理等待锁的线程。线程在获取锁失败后会被包装成节点加入队列,按照 FIFO (First In First Out) 的顺序等待锁的释放。
五、ReentrantLock 与 synchronized 的比较
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 获取方式 | 显式通过 lock()/unlock() 获取和释放 | 隐式获取和释放 |
| 可中断性 | 支持可中断锁(lockInterruptibly()) | 不支持中断 |
| 公平性 | 支持公平锁和非公平锁 | 仅支持非公平锁 |
| 条件变量 | 支持多个条件变量 | 不支持多条件变量 |
| 锁释放 | 必须手动释放(通常在 finally 块中) | 自动释放(即使发生异常) |
| 超时机制 | 支持超时获取锁 | 不支持超时 |
| 尝试获取 | 支持尝试获取锁(tryLock()) | 不支持尝试获取 |
| 性能 | 在高并发下性能略好 | JDK 6 后性能大幅提升,接近 ReentrantLock |
六、ReentrantLock 的使用最佳实践
6.1 始终在 finally 块中释放锁
这是使用 ReentrantLock 最重要的规则,确保即使发生异常,锁也能被正确释放:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 同步代码块
} finally {
// 必须在finally中释放锁
lock.unlock();
}6.2 合理选择公平锁和非公平锁
- 非公平锁(默认):性能更好,但可能导致线程饥饿
- 公平锁:确保线程按照请求顺序获取锁,但性能略差
通常情况下,非公平锁已经足够满足大多数场景的需求:
// 非公平锁(性能更好,推荐使用)
ReentrantLock lock = new ReentrantLock();
// 只有在确实需要公平性保证时才使用公平锁
ReentrantLock fairLock = new ReentrantLock(true);6.3 适当使用可中断锁
在需要响应中断的场景下,使用 lockInterruptibly() 替代 lock():
try {
lock.lockInterruptibly();
try {
// 同步代码块
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断
Thread.currentThread().interrupt();
}6.4 利用超时机制避免死锁
在获取锁时设置合理的超时时间,可以有效避免死锁:
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 同步代码块
} finally {
lock.unlock();
}
} else {
// 超时处理逻辑
log.warn("Failed to acquire lock in 5 seconds");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}6.5 合理使用条件变量
使用条件变量实现线程间通信时,注意以下几点:
- 总是在循环中检查条件,而不是在 if 语句中
- 确保在调用
signal()或signalAll()之前更新了条件状态 - 避免过早或过晚唤醒等待线程
// 正确的条件等待模式
lock.lock();
try {
while (!conditionSatisfied) {
condition.await();
}
// 处理逻辑
} finally {
lock.unlock();
}七、ReentrantLock 的常见问题与解决方案
7.1 忘记释放锁
问题:忘记在 finally 块中释放锁,可能导致死锁
解决方案:严格遵循在 finally 块中释放锁的最佳实践
7.2 锁释放次数少于获取次数
问题:由于可重入性,可能导致锁释放次数少于获取次数,造成锁未完全释放
解决方案:确保每一次 lock() 调用都有对应的 unlock() 调用
// 错误示例
lock.lock();
lock.lock();
try {
// 处理逻辑
} finally {
lock.unlock(); // 只释放了一次,锁未完全释放
}
// 正确示例
lock.lock();
lock.lock();
try {
// 处理逻辑
} finally {
lock.unlock();
lock.unlock(); // 释放两次,锁完全释放
}7.3 线程饥饿
问题:在非公平锁模式下,可能导致某些线程长期无法获取锁
解决方案:
- 对于确实需要公平性的场景,使用公平锁
- 合理设计代码结构,减少锁的持有时间
7.4 条件变量使用不当导致的问题
问题:在 if 语句中检查条件而不是在循环中,可能导致虚假唤醒问题
解决方案:始终在循环中检查条件
// 错误示例
if (!conditionSatisfied) {
condition.await();
}
// 正确示例
while (!conditionSatisfied) {
condition.await();
}八、ReentrantLock 的高级应用示例
8.1 实现生产者-消费者模式
public class BlockingQueue<T> {
private final Queue<T> queue;
private final int capacity;
private final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
public BlockingQueue(int capacity) {
this.queue = new LinkedList<>();
this.capacity = capacity;
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.notFull = lock.newCondition();
}
public void put(T element) throws InterruptedException {
lock.lockInterruptibly();
try {
// 队列满时等待
while (queue.size() == capacity) {
notFull.await();
}
queue.offer(element);
// 通知等待的消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lockInterruptibly();
try {
// 队列空时等待
while (queue.isEmpty()) {
notEmpty.await();
}
T element = queue.poll();
// 通知等待的生产者
notFull.signal();
return element;
} finally {
lock.unlock();
}
}
}8.2 实现可重入读写锁
public class ReentrantReadWriteLockExample {
private final ReentrantLock readLock = new ReentrantLock();
private final ReentrantLock writeLock = new ReentrantLock();
private int readCount = 0;
private final Object readCountLock = new Object();
public void read() {
readLock.lock();
try {
synchronized (readCountLock) {
readCount++;
if (readCount == 1) {
// 第一个读线程获取写锁
writeLock.lock();
}
}
} finally {
readLock.unlock();
}
try {
// 读取操作
System.out.println("Reading data...");
} finally {
readLock.lock();
try {
synchronized (readCountLock) {
readCount--;
if (readCount == 0) {
// 最后一个读线程释放写锁
writeLock.unlock();
}
}
} finally {
readLock.unlock();
}
}
}
public void write() {
writeLock.lock();
try {
// 写入操作
System.out.println("Writing data...");
} finally {
writeLock.unlock();
}
}
}九、总结
ReentrantLock 是 Java 并发编程中功能强大的同步机制,提供了比 synchronized 更灵活的锁操作和控制能力。在需要可中断锁、公平锁、超时获取锁、多条件变量等高级特性的场景下,ReentrantLock 是更好的选择。
使用 ReentrantLock 时,务必遵循最佳实践,特别是始终在 finally 块中释放锁,以确保锁的正确管理和程序的健壮性。在大多数简单的同步场景下,synchronized 仍然是一个不错的选择,因为它使用更简单,且无需手动释放锁。
