ReentrantLock是Java并发包(java.util.concurrent.locks)中提供的一种可重入互斥锁,它作为synchronized关键字的替代方案,提供了更灵活、更强大的线程同步机制。本文将全面解析ReentrantLock的核心特性、实现原理及实际应用场景。
ReentrantLock概述与基本特性
ReentrantLock是Java 5引入的显式锁机制,它基于AQS(AbstractQueuedSynchronizer)框架实现,提供了比synchronized更丰富的功能和控制能力。与synchronized相比,ReentrantLock具有以下显著特点:
- 可重入性:同一线程可以多次获得同一把锁而不会被阻塞,每次获取锁后必须释放相同次数的锁
- 公平性选择:支持公平锁和非公平锁两种策略,公平锁按照线程请求顺序分配锁,非公平锁允许"插队"以提高吞吐量
- 灵活的锁获取方式:提供尝试非阻塞获取锁(tryLock)、可中断获取锁(lockInterruptibly)和超时获取锁(tryLock with timeout)等方法
- 条件变量支持:通过Condition接口实现多个等待队列,比synchronized的wait/notify机制更精准
从实现层级看,synchronized是JVM内置的锁机制,通过monitorenter/monitorexit字节码指令实现;而ReentrantLock是JDK API级别的锁,基于AQS框架构建。
ReentrantLock核心方法与使用
基础锁操作
ReentrantLock的基本使用模式遵循"加锁-操作-释放锁"的流程,必须确保在finally块中释放锁:
csharp
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
这种显式锁管理相比synchronized需要更多注意,但提供了更精细的控制。
高级锁获取方式
-
尝试非阻塞获取锁(tryLock):
立即返回获取结果,不阻塞线程,适用于避免死锁或快速失败场景:
csharpif (lock.tryLock()) { try { // 临界区代码 } finally { lock.unlock(); } } else { // 执行备选方案 }
-
超时获取锁:
在指定时间内尝试获取锁,避免无限期等待:
csharpif (lock.tryLock(2, TimeUnit.SECONDS)) { try { // 临界区代码 } finally { lock.unlock(); } }
-
可中断获取锁(lockInterruptibly):
允许在等待锁的过程中响应中断信号:
csharptry { lock.lockInterruptibly(); try { // 临界区代码 } finally { lock.unlock(); } } catch (InterruptedException e) { // 处理中断 }
锁状态查询
ReentrantLock提供了一系列状态查询方法:
isLocked()
:查询锁是否被持有isHeldByCurrentThread()
:当前线程是否持有锁getHoldCount()
:当前线程持有锁的次数(重入次数)getQueueLength()
:等待获取锁的线程数
ReentrantLock实现原理
AQS框架基础
ReentrantLock的核心实现依赖于AbstractQueuedSynchronizer(AQS),这是一个用于构建锁和同步器的框架。AQS内部维护了:
- volatile int state:同步状态,对于ReentrantLock,0表示未锁定,>0表示锁定状态及重入次数
- FIFO线程等待队列:管理获取锁失败的线程
公平锁与非公平锁实现
ReentrantLock通过两种不同的Sync子类实现锁策略:
-
非公平锁(默认):
scssfinal void lock() { if (compareAndSetState(0, 1)) // 直接尝试抢占 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
新请求的线程可以直接插队尝试获取锁,不考虑等待队列
-
公平锁:
arduinoprotected final boolean tryAcquire(int acquires) { if (!hasQueuedPredecessors() && // 检查是否有前驱节点 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } // 可重入逻辑... }
严格按照FIFO顺序分配锁,避免饥饿现象
锁的获取与释放流程
-
加锁过程:
- 尝试通过CAS修改state状态
- 成功则设置当前线程为独占线程
- 失败则构造Node加入CLH队列尾部,并阻塞线程
-
释放过程:
- 减少持有计数(state减1)
- 当state为0时完全释放锁
- 唤醒队列中的下一个等待线程
ReentrantLock实战应用
生产者-消费者模型
使用ReentrantLock配合Condition实现高效的生产者-消费者模式:
ini
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final Object[] items = new Object[100];
private int putPtr, takePtr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 等待"不满"条件
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(); // 等待"不空"条件
Object x = items[takePtr];
if (++takePtr == items.length) takePtr = 0;
--count;
notFull.signal(); // 通知"不满"条件
return x;
} finally {
lock.unlock();
}
}
}
这种实现比synchronized+wait/notify更高效,因为可以精准唤醒生产者或消费者线程。
银行转账避免死锁
使用tryLock实现带超时的转账操作,避免死锁:
vbnet
public boolean transfer(Account from, Account to, int amount, long timeout, TimeUnit unit) {
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
if (from.getLock().tryLock()) {
try {
if (to.getLock().tryLock()) {
try {
if (from.getBalance() < amount)
throw new InsufficientFundsException();
from.withdraw(amount);
to.deposit(amount);
return true;
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
if (System.nanoTime() > stopTime)
return false;
Thread.sleep(fixedDelay);
}
}
通过tryLock和超时机制,有效预防了死锁风险。
可中断的任务执行
使用lockInterruptibly实现可中断的任务执行:
csharp
public class InterruptibleTask {
private final ReentrantLock lock = new ReentrantLock();
public void executeTask() throws InterruptedException {
lock.lockInterruptibly();
try {
// 执行可能长时间运行的任务
while (!Thread.currentThread().isInterrupted()) {
// 任务逻辑...
}
} finally {
lock.unlock();
}
}
}
这种模式适用于需要支持任务取消的场景。
ReentrantLock与synchronized的对比
特性 | synchronized | ReentrantLock |
---|---|---|
实现层级 | JVM内置 | JDK API实现 |
锁释放 | 自动 | 必须手动调用unlock() |
公平锁支持 | 仅非公平 | 支持公平和非公平策略 |
可中断获取锁 | 不支持 | 支持(lockInterruptibly) |
超时获取锁 | 不支持 | 支持(tryLock with timeout) |
条件变量 | 单一等待队列 | 支持多个Condition |
锁状态查询 | 有限 | 提供丰富查询方法 |
性能 | Java 6+优化 | 复杂场景下表现更好 |
代码简洁性 | 高 | 较低(需手动管理) |
适用场景 | 简单同步 | 复杂同步需求 |
在Java 6及以后版本中,synchronized经过锁升级(偏向锁→轻量级锁→重量级锁)优化,性能与ReentrantLock差距已不明显。因此,简单场景推荐使用synchronized,复杂场景才考虑ReentrantLock。
ReentrantLock最佳实践
-
始终在finally块中释放锁:
确保锁一定会被释放,避免死锁:
csharplock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
-
避免嵌套锁:
尽量不要在持有一个锁的情况下尝试获取另一个锁,容易导致死锁。
-
合理选择锁策略:
- 高吞吐场景:非公平锁(默认)
- 避免饥饿场景:公平锁
-
优先使用tryLock:
特别是涉及多个锁的操作,使用tryLock可以避免死锁。
-
合理使用Condition:
替代Object的wait/notify,实现更精准的线程通信。
-
性能考量:
简单同步场景优先选择synchronized,复杂场景才使用ReentrantLock。
总结
ReentrantLock作为Java并发编程中的重要工具,通过其可重入性、公平性选择、灵活的锁获取方式和条件变量支持,为开发者提供了比synchronized更强大的线程同步能力。理解其基于AQS的实现原理,掌握各种高级特性的使用方法,并遵循最佳实践,可以帮助我们构建更高效、更健壮的并发程序。在实际开发中,应根据具体场景需求,在synchronized和ReentrantLock之间做出合理选择。