ReentrantLock 是 Java java.util.concurrent.locks 包下提供的显式锁实现,它是 JDK 1.5 引入并发包(JUC)时的核心组件之一。与内置关键字 synchronized 不同,ReentrantLock 提供了更灵活的锁获取机制、更高的可控性以及丰富的功能特性。
以下从核心概念、底层原理、API 详解、与 synchronized 对比、最佳实践五个维度进行深度解析。
1. 核心概念与特性
1.1 什么是 ReentrantLock?
ReentrantLock 是一个可重入的互斥锁(Exclusive Lock)。
- 可重入性 (Reentrancy):同一个线程可以多次获取同一把锁而不会发生死锁。内部通过计数器记录重入次数,每次
lock()计数 +1,每次unlock()计数 -1,直到计数为 0 才真正释放锁。 - 互斥性:同一时刻只有一个线程能持有该锁。
1.2 核心特性
- 公平与非公平选择:
- 非公平锁(默认):新来的线程可以直接尝试抢占锁,无需排队。吞吐量高,但可能导致某些线程长期饥饿。
- 公平锁:线程按照请求锁的顺序(FIFO)获取锁。避免了饥饿,但上下文切换开销大,吞吐量略低。
- 可中断等待:支持
lockInterruptibly(),允许等待锁的线程响应中断信号,从而避免死锁或长时间无效等待。 - 超时机制:支持
tryLock(long timeout, TimeUnit unit),若在指定时间内未获取到锁则返回 false,防止无限阻塞。 - 多条件变量 (Condition):相比
synchronized单一的wait/notify,ReentrantLock可以绑定多个Condition对象,实现精准的线程唤醒(如:分别唤醒“读线程”和“写线程”)。
2. 底层实现原理:AQS
ReentrantLock 的核心基于 AQS (AbstractQueuedSynchronizer) 框架实现。
2.1 类结构
public class ReentrantLock implements Lock {
// 内部同步器,继承自 AQS
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
}
// 非公平锁实现
static final class NonfairSync extends Sync { ... }
// 公平锁实现
static final class FairSync extends Sync { ... }
}
2.2 AQS 核心状态管理
AQS 使用一个 volatile int state 变量来表示同步状态:
state = 0:锁空闲。state > 0:锁被持有,数值代表重入次数。exclusiveOwnerThread:记录当前持有锁的线程。
2.3 加锁流程 (以非公平锁为例)
- CAS 尝试快速加锁:
- 检查
state是否为 0。 - 如果是 0,通过 CAS (
compareAndSetState) 将state设为 1,并设置当前线程为持有者。成功则返回。 - 这是非公平锁“插队”的关键步骤。
- 检查
- 重入判断:
- 如果
state != 0,但当前线程就是持有者,则state++,返回成功。
- 如果
- 进入队列等待:
- 如果上述两步都失败,调用
acquire(1)。 - 将当前线程封装成
Node节点,加入 AQS 的 CLH 双向队列尾部。 - 线程挂起(Park),等待前驱节点释放锁后唤醒。
- 如果上述两步都失败,调用
2.4 解锁流程
- 检查当前线程是否持有锁。
state--。- 如果
state == 0,清除持有者标记,并唤醒队列中第一个等待节点(Head 的后继节点)。 - 如果
state > 0,说明是重入锁,仅减少计数,不唤醒其他线程。
3. API 详解与使用场景
ReentrantLock 提供了四种主要的加锁方式,适用于不同场景:
表格
| 方法 | 行为描述 | 适用场景 |
|---|---|---|
lock() | 阻塞获取锁。若锁不可用,当前线程休眠直到获取锁。不可中断。 | 大多数常规业务场景,确保最终能执行临界区代码。 |
tryLock() | 非阻塞尝试获取锁。立即返回 true (成功) 或 false (失败)。不等待。 | 乐观锁场景、避免死锁、轮询检查资源。即使配置了公平锁,此方法也是非公平的(只要锁空闲就抢)。 |
tryLock(long time, TimeUnit unit) | 限时等待。在指定时间内尝试获取锁,超时返回 false。可中断。 | 对响应时间有要求的场景,防止线程无限期等待导致系统雪崩。 |
lockInterruptibly() | 阻塞获取锁,但响应中断。若线程在等待期间被中断,抛出 InterruptedException。 | 需要灵活控制线程生命周期的场景,如取消长时间运行的任务。 |
代码示例
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
public void businessMethod() {
// 1. 加锁
lock.lock();
try {
// 2. 临界区业务逻辑
System.out.println("Thread " + Thread.currentThread().getName() + " is working...");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
} finally {
// 3. 务必在 finally 中释放锁,防止死锁
lock.unlock();
}
}
public void tryLockMethod() {
// 尝试获取锁,最多等待 2 秒
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
// 获取锁失败,执行降级逻辑或重试
System.out.println("Failed to acquire lock, busy.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
4. ReentrantLock vs Synchronized
虽然 JDK 1.6 之后 synchronized 经过偏向锁、轻量级锁等优化,性能大幅提升,但在特定场景下 ReentrantLock 仍具有不可替代的优势。
表格
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM 层面 (C++),通过 monitorenter/monitorexit 指令。 | JDK 层面 (Java),基于 AQS 框架。 |
| 锁释放 | 自动释放(代码块结束或异常抛出时)。 | 必须手动释放,需在 finally 块中调用 unlock()。 |
| 公平性 | 仅支持非公平锁。 | 支持公平锁和非公平锁(构造时指定)。 |
| 中断响应 | 等待锁时不可中断。 | 支持 lockInterruptibly(),可响应中断。 |
| 超时控制 | 不支持超时获取。 | 支持 tryLock(timeout),可设置超时时间。 |
| 条件等待 | 配合 wait()/notify(),只有一个等待队列,唤醒随机或全部。 | 配合 Condition,可创建多个等待队列,实现精准唤醒(如 signalAll() 针对特定条件)。 |
| 性能 | 低竞争下性能优异,高竞争下由于自旋和重量级锁切换,开销较大。 | 高竞争下通常表现更稳定,因为可以通过自旋+队列机制减少上下文切换。 |
| 调试监控 | 难以查看锁状态。 | 提供 getHoldCount(), isLocked(), getQueueLength() 等方法,便于监控和诊断。 |
选型建议:
- 优先使用
synchronized:代码简洁,不易出错(自动释放),适用于大多数简单的同步场景。 - 使用
ReentrantLock:- 需要公平锁策略。
- 需要尝试获取锁(
tryLock)或超时机制。 - 需要在等待锁时响应中断。
- 需要多个条件变量进行精细化的线程通信。
- 高并发竞争场景下需要更稳定的性能表现。
5. 注意事项与最佳实践
-
必须在 Finally 中解锁:
ReentrantLock不会自动释放锁。如果业务逻辑抛出异常且未在finally中解锁,其他所有等待该锁的线程将永久阻塞,导致系统瘫痪。lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock(); // 确保执行
} -
锁的重入次数限制:
ReentrantLock的最大重入次数为Integer.MAX_VALUE。虽然极少触及,但在极端递归场景下需注意Error: Maximum lock count exceeded。 -
公平锁的性能代价:
公平锁为了保证 FIFO 顺序,每次加锁都需要检查队列,这会导致大量的上下文切换和线程挂起/唤醒操作。在高吞吐场景下,非公平锁的性能通常远高于公平锁。除非有严格的顺序需求,否则建议使用默认的非公平锁。 -
避免锁泄露:
确保lock()和unlock()成对出现。不要在try块之前加锁,也不要在catch块中遗漏解锁。 -
Condition 的正确使用:
使用condition.await()时,必须确保当前线程持有锁,否则会抛出IllegalMonitorStateException。同样,await()会释放锁并挂起线程,被唤醒后会重新竞争锁。
总结
ReentrantLock 是 Java 并发编程中强大且灵活的工具。它基于 AQS 框架,通过 CAS 和 CLH 队列实现了高效的并发控制。虽然使用复杂度高于 synchronized,但其提供的公平性选择、可中断、超时控制和多条件变量等功能,使其成为解决复杂并发问题(如生产者-消费者模型、读写分离、分布式锁本地实现等)的首选方案。掌握其底层原理和使用规范,是进阶 Java 高级开发的必经之路。

4326

被折叠的 条评论
为什么被折叠?



