ReentrantLock详解
1. 基本概念
ReentrantLock 是 Java 并发包 (java.util.concurrent.locks) 中提供的一个可重入锁实现,是 Lock 接口的一个实现类。“可重入"意味着同一个线程可以多次获取同一把锁而不会死锁。
与 synchronized 关键字相比,ReentrantLock 提供了更加灵活和强大的锁机制:
- 可中断锁获取
- 可设置超时时间
- 可实现公平锁
- 可实现非块结构的加锁
2. 基本用法
Lock lock = new ReentrantLock();
try {
// 获取锁
lock.lock();
// 临界区代码
// ...
} finally {
// 释放锁
lock.unlock();
}
注意事项:
- 必须在 finally 块中释放锁,确保锁一定会被释放
- 锁的获取和释放必须成对出现
3. 高级特性
3.1 可中断锁获取
Lock lock = new ReentrantLock();
try {
// 可中断地获取锁
lock.lockInterruptibly();
// 临界区代码
} catch (InterruptedException e) {
// 处理中断异常
} finally {
// 如果当前线程持有锁,则释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
3.2 超时锁获取
Lock lock = new ReentrantLock();
boolean acquired = false;
try {
// 尝试在指定时间内获取锁
acquired = lock.tryLock(5, TimeUnit.SECONDS);
if (acquired) {
// 成功获取锁,执行临界区代码
} else {
// 获取锁超时,执行替代逻辑
}
} catch (InterruptedException e) {
// 处理中断异常
} finally {
// 如果获取了锁,则释放锁
if (acquired) {
lock.unlock();
}
}
3.3 公平锁与非公平锁
ReentrantLock 默认创建的是非公平锁,可以通过构造函数参数创建公平锁:
// 创建公平锁
Lock fairLock = new ReentrantLock(true);
// 创建非公平锁(默认)
Lock unfairLock = new ReentrantLock(false);
- 公平锁:线程按照请求锁的顺序获取锁,等待时间最长的线程优先获取锁
- 非公平锁:线程可以"插队"获取锁,性能通常更好,但可能导致某些线程长时间等待(饥饿)
3.4 Condition 条件变量
ReentrantLock 可以创建多个条件变量,用于线程间的协作:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待条件
lock.lock();
try {
while (!conditionMet) {
condition.await();
}
// 条件满足,执行操作
} finally {
lock.unlock();
}
// 通知条件已满足
lock.lock();
try {
// 改变条件状态
conditionMet = true;
condition.signal(); // 或 condition.signalAll();
} finally {
lock.unlock();
}
4. 内部实现原理
4.1 基本结构
ReentrantLock 内部依赖于 AbstractQueuedSynchronizer (AQS) 实现,它有两个内部类:
- FairSync:公平锁实现
- NonfairSync:非公平锁实现
这两个类都继承自 AbstractQueuedSynchronizer,通过重写 AQS 的方法实现不同的锁语义。
4.2 AQS 核心原理
AQS 是 Java 并发包中的核心框架,提供了一个基于 FIFO 队列的阻塞锁和相关同步器的实现。AQS 的核心是:
- 一个 volatile 的 int 类型状态变量 (state)
- 一个 FIFO 的等待队列
- 线程阻塞和唤醒的机制
对于 ReentrantLock:
- state = 0 表示锁未被占用
- state > 0 表示锁被占用,值表示重入次数
4.3 锁获取过程
以非公平锁为例,获取锁的基本流程:
- 尝试通过 CAS 操作将 state 从 0 改为 1
- 如果成功,设置当前线程为锁的独占线程
- 如果失败,检查当前持有锁的线程是否是自己(重入情况)
- 如果是,增加 state 值
- 如果不是,将当前线程包装成 Node 加入等待队列,并阻塞当前线程
公平锁的区别在于,它会先检查队列中是否有等待的线程,如果有则不会尝试获取锁,而是直接进入队列等待。
4.4 锁释放过程
- 减少 state 值
- 当 state 变为 0 时,清除独占线程标记
- 唤醒等待队列中的下一个线程
4.5 可重入实现
ReentrantLock 的可重入是通过记录获取锁的线程标识和重入次数实现的:
- 当一个线程获取锁时,记录这个线程的标识(Thread.currentThread())
- 同一个线程再次获取锁时,检查是否是已持有锁的线程,如果是则增加重入计数
- 释放锁时,减少重入计数,当计数为 0 时才真正释放锁
5. ReentrantLock vs synchronized
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 锁释放 | 显式调用 unlock() | 自动释放 |
| 公平性 | 可选择公平/非公平 | 非公平 |
| 中断响应 | 支持 | 不支持 |
| 超时获取 | 支持 | 不支持 |
| 多条件变量 | 支持 | 不支持(只有一个条件变量) |
| 性能 | JDK 1.6 后与 synchronized 接近 | JDK 1.6 后进行了优化 |
| 使用难度 | 稍复杂,需要手动释放锁 | 简单,语法层面支持 |
6. 使用场景
ReentrantLock 适用于以下场景:
- 需要高级功能如可中断、超时获取、公平性的场景
- 需要多个条件变量的场景
- 需要非阻塞地尝试获取锁的场景
- 需要实现可中断的锁获取操作的场景
synchronized 适用于:
- 简单的同步需求
- 不需要高级特性的场景
- 追求代码简洁性的场景
7. 最佳实践
- 始终在 finally 块中释放锁
- 避免在持有锁的情况下调用外部方法
- 尽量减少锁的持有时间
- 优先考虑使用 synchronized,除非需要 ReentrantLock 的特殊功能
- 使用 try-with-resources 简化锁的管理(Java 8+)
// 实现 AutoCloseable 接口的锁包装类
class AutoCloseableLock implements AutoCloseable {
private final Lock lock;
public AutoCloseableLock(Lock lock) {
this.lock = lock;
this.lock.lock();
}
@Override
public void close() {
this.lock.unlock();
}
}
// 使用示例
Lock lock = new ReentrantLock();
try (AutoCloseableLock l = new AutoCloseableLock(lock)) {
// 临界区代码
}
8. 总结
ReentrantLock 是 Java 并发编程中一个强大的工具,它提供了比 synchronized 更多的功能和灵活性。通过深入理解其内部实现原理和使用方法,可以在适当的场景下选择使用它来解决复杂的并发问题。
然而,在大多数简单的同步场景下,synchronized 关键字仍然是首选,因为它使用更简单,且 JVM 对其进行了大量优化。只有在确实需要 ReentrantLock 提供的高级特性时,才应该选择使用它。