1. ReentrantLock 的基本概念与核心特性
ReentrantLock 是 Java 并发包中提供的一种可重入互斥锁,它实现了 Lock 接口,提供了比 synchronized 更灵活的锁机制。与 synchronized 相比,ReentrantLock 具有以下显著特点:
- 可重入性:同一个线程可以多次获取同一把锁,而不会导致死锁
- 公平性选择:支持公平锁和非公平锁两种模式
- 锁中断:支持响应中断的获取锁方式
- 超时机制:可以尝试在指定时间内获取锁
- 条件变量:支持多个条件队列
在底层实现上,ReentrantLock 依赖于 AbstractQueuedSynchronizer(AQS)框架。AQS 使用一个 volatile int 类型的 state 变量来表示同步状态,并通过一个 FIFO 队列来管理获取锁失败的线程。
重要提示:使用 ReentrantLock 时必须显式调用 unlock() 释放锁,否则会导致死锁。建议在 finally 块中释放锁。
1.1 可重入性实现原理
可重入性是 ReentrantLock 的核心特性之一。当一个线程第一次获取锁时,AQS 会将 state 从 0 改为 1,并记录当前持有锁的线程。如果同一个线程再次获取锁,state 会递增,而不会阻塞线程。释放锁时,state 会递减,只有当 state 回到 0 时才完全释放锁。
java复制final boolean nonfairTryAcquire(int acquires) {
final 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) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这段代码展示了非公平锁获取锁的逻辑。可以看到,当当前线程已经是锁的持有者时,会直接增加 state 值而不是阻塞。
1.2 公平锁与非公平锁的区别
ReentrantLock 提供了公平和非公平两种模式:
- 公平锁:严格按照请求顺序分配锁,先到先得
- 非公平锁:允许插队,新来的线程可能比等待队列中的线程先获取锁
公平锁的实现是通过 hasQueuedPredecessors() 方法检查是否有更早的线程在等待:
java复制protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// ... 省略可重入部分
}
非公平锁通常有更高的吞吐量,但可能导致某些线程长时间获取不到锁(饥饿)。在实际应用中,除非有严格的公平性要求,否则非公平锁通常是更好的选择。
2. ReentrantLock 的底层实现机制
2.1 AQS 框架的核心设计
AbstractQueuedSynchronizer(AQS)是 ReentrantLock 实现的基础框架。AQS 使用一个双向链表(CLH 队列变种)来管理等待线程,并通过一个 volatile int 类型的 state 变量表示同步状态。
AQS 的核心数据结构包括:
- state:同步状态,对于 ReentrantLock 表示锁的重入次数
- head/tail:等待队列的头尾指针
- Node:表示等待线程的节点,包含线程引用、等待状态等信息
当线程获取锁失败时,会创建一个 Node 并加入队列尾部,然后进入等待状态。当锁释放时,会唤醒队列中的下一个线程。
2.2 锁获取与释放的完整流程
获取锁的流程:
- 尝试通过 CAS 操作获取锁(非公平锁直接尝试)
- 如果失败,将当前线程包装为 Node 加入等待队列
- 进入自旋状态,检查前驱节点是否为头节点并尝试获取锁
- 如果满足条件则获取锁,否则可能被挂起
释放锁的流程:
- 减少 state 值
- 如果 state 变为 0,表示完全释放锁
- 唤醒等待队列中的下一个线程
java复制// 释放锁的核心代码
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2.3 条件变量的实现原理
ReentrantLock 通过 ConditionObject 内部类实现条件变量功能。与 Object 的 wait/notify 相比,Condition 提供了更灵活的条件等待机制:
- 一个锁可以创建多个 Condition
- 支持中断等待
- 支持超时等待
条件变量的实现依赖于另一个等待队列。当调用 await() 时,当前线程会释放锁并加入条件队列;当调用 signal() 时,会将条件队列中的线程转移到锁的等待队列中。
3. ReentrantLock 的业务场景应用
3.1 高并发场景下的资源保护
在电商秒杀系统中,库存扣减是一个典型的需要严格同步的场景。使用 ReentrantLock 可以确保库存扣减的原子性:
java复制public class InventoryService {
private final ReentrantLock lock = new ReentrantLock();
private int stock = 100;
public boolean reduceStock() {
lock.lock();
try {
if (stock > 0) {
stock--;
return true;
}
return false;
} finally {
lock.unlock();
}
}
}
3.2 复杂事务中的细粒度控制
在需要跨多个方法调用的复杂事务中,ReentrantLock 的可重入特性非常有用:
java复制public class TransactionManager {
private final ReentrantLock lock = new ReentrantLock();
public void beginTransaction() {
lock.lock();
// 初始化事务
}
public void commit() {
try {
// 提交事务
} finally {
lock.unlock();
}
}
public void rollback() {
try {
// 回滚事务
} finally {
lock.unlock();
}
}
}
3.3 条件变量的典型应用场景
在生产者-消费者模型中,可以使用 Condition 实现更精确的线程协调:
java复制public class BoundedBuffer {
final Lock 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();
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();
}
}
}
4. ReentrantLock 的高级用法与性能优化
4.1 锁分段技术提升并发度
对于高度竞争的资源,可以采用锁分段技术减少锁的争用:
java复制public class StripedMap {
private static final int N_LOCKS = 16;
private final Node[] buckets;
private final ReentrantLock[] locks;
public StripedMap(int capacity) {
buckets = new Node[capacity];
locks = new ReentrantLock[N_LOCKS];
for (int i = 0; i < N_LOCKS; i++)
locks[i] = new ReentrantLock();
}
private final int hash(Object key) {
return Math.abs(key.hashCode() % buckets.length);
}
public Object get(Object key) {
int hash = hash(key);
lockFor(hash).lock();
try {
for (Node m = buckets[hash]; m != null; m = m.next)
if (m.key.equals(key))
return m.value;
return null;
} finally {
lockFor(hash).unlock();
}
}
private ReentrantLock lockFor(int hash) {
return locks[hash % N_LOCKS];
}
static class Node {
Object key, value;
Node next;
}
}
4.2 锁超时与中断处理
在分布式锁等场景中,锁超时机制可以防止死锁:
java复制public boolean tryLockWithTimeout(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
lock.lockInterruptibly();
try {
while (resourceNotAvailable()) {
if (nanos <= 0)
return false;
nanos = condition.awaitNanos(nanos);
}
// 获取资源
return true;
} finally {
lock.unlock();
}
}
4.3 锁的监控与诊断
通过扩展 ReentrantLock 可以实现锁的监控:
java复制public class MonitorableReentrantLock extends ReentrantLock {
public Thread getOwnerThread() {
return super.getOwner();
}
public Collection<Thread> getQueuedThreads() {
return super.getQueuedThreads();
}
public int getQueueLength() {
return super.getQueueLength();
}
}
在实际生产环境中,可以通过这些监控接口实现死锁检测、锁争用分析等功能。
5. ReentrantLock 与 synchronized 的深度对比
5.1 性能差异分析
在低竞争场景下,synchronized 的性能通常优于 ReentrantLock,因为 JVM 对 synchronized 进行了大量优化(如偏向锁、轻量级锁)。但在高竞争场景下,ReentrantLock 通常能提供更好的吞吐量。
5.2 功能特性对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 公平锁 | 支持 | 不支持 |
| 锁中断 | 支持 | 不支持 |
| 超时机制 | 支持 | 不支持 |
| 条件变量 | 支持多个 | 只支持一个 |
| 锁释放 | 必须显式调用 unlock() | 自动释放 |
| 性能 | 高竞争下表现更好 | 低竞争下优化更好 |
5.3 选择建议
-
优先考虑 synchronized 的情况:
- 简单的同步需求
- 锁的获取和释放在一个方法内完成
- 对性能要求不高的小规模并发
-
优先考虑 ReentrantLock 的情况:
- 需要高级功能(如可中断、超时、公平性)
- 跨方法的复杂同步逻辑
- 高竞争环境下的性能优化
- 需要监控锁状态
6. 实际应用中的常见问题与解决方案
6.1 锁泄漏问题
锁泄漏是指获取锁后没有正确释放,通常是由于忘记在 finally 块中调用 unlock() 或代码抛出异常导致的。解决方法:
- 使用 try-finally 确保锁释放
- 使用 try-with-resources 模式(Java 7+)
- 使用 lombok 的 @Cleanup 注解
java复制// 正确的锁释放方式
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
6.2 死锁预防
虽然 ReentrantLock 支持可中断和超时,但仍需注意死锁问题。预防措施包括:
- 按固定顺序获取多个锁
- 使用 tryLock 设置超时
- 实现死锁检测机制
- 避免在持有锁时调用外部方法
6.3 性能调优建议
- 减少锁的持有时间:将不需要同步的代码移出临界区
- 减小锁的粒度:使用更细粒度的锁或锁分段
- 避免过度同步:只在必要时使用锁
- 考虑读写锁:读多写少的场景使用 ReadWriteLock
- 监控锁争用:通过 JMX 或自定义监控发现热点锁
7. ReentrantLock 在分布式环境下的应用思考
虽然 ReentrantLock 是 JVM 级别的锁,但在分布式系统中也有其应用价值:
7.1 与分布式锁的配合使用
在分布式系统中,可以将 ReentrantLock 与分布式锁(如 Redis 锁、Zookeeper 锁)结合使用:
- 先获取本地 ReentrantLock
- 再尝试获取分布式锁
- 释放时先释放分布式锁,再释放本地锁
这种组合可以减少对分布式锁的争用,提高性能。
7.2 分布式锁的本地缓存
对于频繁访问的资源,可以在本地使用 ReentrantLock 保护一个缓存版本:
java复制public class DistributedResourceProxy {
private final ReentrantLock lock = new ReentrantLock();
private volatile Resource cachedResource;
private volatile long lastUpdateTime;
public Resource getResource() {
// 先检查本地缓存
if (System.currentTimeMillis() - lastUpdateTime < 1000) {
return cachedResource;
}
lock.lock();
try {
// 双重检查
if (System.currentTimeMillis() - lastUpdateTime < 1000) {
return cachedResource;
}
// 从分布式系统获取最新资源
Resource newResource = fetchFromRemote();
cachedResource = newResource;
lastUpdateTime = System.currentTimeMillis();
return newResource;
} finally {
lock.unlock();
}
}
}
7.3 分布式事务的本地协调
在实现分布式事务的补偿机制时,可以使用 ReentrantLock 来协调本地资源的状态变更,确保在补偿过程中的一致性。
我在实际项目中使用 ReentrantLock 时发现,合理设置锁的超时时间非常重要。过短的超时会导致不必要的重试,过长的超时则可能掩盖系统问题。通常我会根据业务特点设置不同的超时策略,比如对于支付操作使用较长的超时(5-10秒),而对于库存查询则使用较短的超时(100-300毫秒)。
