ReentrantLock 深度解析:告别 synchronized,解锁 Java 并发编程新境界
在 Java 并发编程的世界里,synchronized 关键词几乎是所有开发者的启蒙,随着业务复杂度的提升,synchronized 的局限性也逐渐显现。java.util.concurrent.locks.ReentrantLock 作为 synchronized 的有力补充,提供了更强大、更灵活的锁机制,本文将从基础到进阶,全方位剖析 ReentrantLock 的核心原理、使用方法、与 synchronized 的对比,以及在实际开发中的最佳实践,助你彻底掌握这一并发利器。

开篇:为什么我们需要 ReentrantLock?
想象一个场景:你正在开发一个高并发的秒杀系统,需要对库存进行扣减,使用 synchronized 关键字,代码可能是这样的:
public class SynchronizedExample {
private int stock = 100;
public synchronized void decrement() {
if (stock > 0) {
stock--;
System.out.println(Thread.currentThread().getName() + " 扣减成功,剩余库存: " + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 扣减失败,库存不足");
}
}
}
这段代码简单有效,但很快你就会遇到新的需求:
- 尝试获取锁:如果锁被其他线程占用,我不想一直等待,而是想立即返回“失败”状态,去做其他事情。
- 公平锁:我希望锁能分配给等待时间最长的线程,而不是随机分配,避免“饥饿”现象。
- 可中断地获取锁:如果线程在等待锁的过程中,我想通过中断(
interrupt())的方式让它放弃等待。 - 获取锁的超时控制:我只想等待一定的时间,500ms,如果还没拿到锁,就放弃。
synchronized 关键字无法满足这些精细化的控制需求,这时,ReentrantLock 便闪亮登场,它就像一个功能强大的“瑞士军刀”,为解决复杂的并发问题提供了可能。
ReentrantLock 是什么?
ReentrantLock 是 java.util.concurrent.locks 包下的一个类,它实现了 Lock 接口,从名字可以看出,它是一个可重入锁。

可重入(Reentrant) 的意思是,一个已经获取了锁的线程,可以再次获取同一个锁而不会死锁,这和 synchronized 的行为是一致的,都是为了支持在同步代码块中调用其他同步方法。
ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("进入 methodA");
methodB(); // 在持有锁的情况下再次获取锁
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
System.out.println("进入 methodB");
} finally {
lock.unlock();
}
}
线程调用 methodA() 获取锁后,再调用 methodB() 时,可以成功再次获取锁,而不会发生死锁。
ReentrantLock 的核心用法与 API
使用 ReentrantLock 的标准模式是 try-finally,以确保锁一定会被释放,避免死锁。
Lock lock = new ReentrantLock();
public void doSomething() {
lock.lock(); // 获取锁
try {
// 临界区(被保护的代码)
System.out.println("线程 " + Thread.currentThread().getName() + " 正在工作...");
} finally {
lock.unlock(); // 释放锁
}
}
ReentrantLock 提供了比 synchronized 更丰富的 API,主要体现在获取锁的方式上:

lock() / unlock() - 基础方法
lock():尝试获取锁,如果锁被其他线程占用,则当前线程会一直阻塞,直到获取到锁。unlock():释放锁,必须在try-finally块中调用,以确保异常发生时锁也能被释放。
tryLock() - 尝试获取锁
tryLock() 是 ReentrantLock 的精髓之一,它允许线程在尝试获取锁时不被阻塞。
tryLock():尝试获取锁,如果成功,返回true;如果锁被其他线程占用,立即返回false,线程继续执行后续代码。
public void tryLockExample() {
if (lock.tryLock()) { // 尝试获取锁,立即返回结果
try {
System.out.println("成功获取锁,执行任务");
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败,执行其他任务或稍后重试");
}
}
tryLock(long time, TimeUnit unit):在指定的时间内尝试获取锁,如果在超时时间内获取到锁,返回true;如果超时仍未获取,返回false。
public void tryLockWithTimeoutExample() throws InterruptedException {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { // 最多等待500ms
try {
System.out.println("在超时时间内成功获取锁");
} finally {
lock.unlock();
}
} else {
System.out.println("等待500ms后仍未获取到锁");
}
}
lockInterruptibly() - 可中断地获取锁
当一个线程通过 lockInterruptibly() 获取锁时,如果它在等待过程中被其他线程中断(interrupt()),它会响应中断,并抛出 InterruptedException,然后立即返回。
public void lockInterruptiblyExample() throws InterruptedException {
lock.lockInterruptibly(); // 可以响应中断的获取锁方式
try {
System.out.println("成功获取锁,开始执行任务");
// ... 长时间任务
} finally {
lock.unlock();
}
}
这在需要实现“取消”或“超时”逻辑的场景中非常有用,避免了线程无限期等待。
newCondition() - 条件变量
ReentrantLock 可以关联一个或多个 Condition 对象,相当于 synchronized 内置的 wait()/notify()/notifyAll() 机制的增强版。
await():相当于Object.wait(),当前线程释放锁并进入等待状态。signal():相当于Object.notify(),唤醒一个等待的线程。signalAll():相当于Object.notifyAll(),唤醒所有等待的线程。
经典示例:生产者-消费者模型
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ProducerConsumerExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private int buffer = 0;
private final int MAX_BUFFER = 10;
public void produce() throws InterruptedException {
lock.lock();
try {
while (buffer == MAX_BUFFER) {
System.out.println("缓冲区已满,生产者等待...");
condition.await(); // 释放锁并等待
}
buffer++;
System.out.println("生产者生产,当前库存: " + buffer);
condition.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (buffer == 0) {
System.out.println("缓冲区为空,消费者等待...");
condition.await(); // 释放锁并等待
}
buffer--;
System.out.println("消费者消费,当前库存: " + buffer);
condition.signal(); // 唤醒生产者
} finally {
lock.unlock();
}
}
}
使用 Condition 可以精确地唤醒指定的线程(生产者唤醒消费者,消费者唤醒生产者),避免了 notifyAll() 带来的无效唤醒和上下文切换开销。
ReentrantLock vs. synchronized:一场巅峰对决
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可中断获取锁 | 支持 (lockInterruptibly) |
不支持 |
| 尝试获取锁 | 支持 (tryLock()) |
不支持 |
| 公平锁 | 支持 (构造函数参数) | 不支持(非公平锁) |
| 绑定多个条件变量 | 支持 (newCondition()) |
不支持(仅一个内置条件) |
| 锁的释放 | 必须在 finally 中手动 unlock() |
JVM 自动释放,异常时也能释放 |
| 底层实现 | AQS (AbstractQueuedSynchronizer) |
JVM 实现的 monitor 机制 |
| 使用灵活性 | 高 | 低 |
| 使用便捷性 | 低(需手动加解锁) | 高(关键字,JVM 管理) |
| 性能 (早期) | 性能更好(尤其在高竞争下) | 性能稍差 |
| 性能 (JDK 1.6+) | 差距不大,JVM 对 synchronized 优化显著(偏向锁、轻量级锁) |
性能已大幅提升 |
总结与选择建议:
- 优先使用
synchronized:对于绝大多数简单的同步场景,synchronized代码更简洁、不易出错,JVM 的持续优化使其性能已经非常出色,它是 Java 并发的“第一选择”。 - 选择 ReentrantLock:当你的业务场景满足以下任何一个或多个需求时,果断选择 ReentrantLock:
- 需要尝试获取锁(
tryLock)。 - 需要可中断地获取锁(
lockInterruptibly)。 - 需要实现公平锁,避免线程饥饿。
- 需要精确地唤醒多个等待队列中的线程(多条件变量)。
- 需要尝试获取锁(
实战案例:实现一个公平的打印服务
假设我们有一个打印任务队列,多个线程需要提交打印任务,我们希望先提交的任务先被处理(公平性),可以使用 ReentrantLock 的公平锁模式。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairPrinterService {
// 使用公平锁
private final Lock lock = new ReentrantLock(true);
private int currentJob = 0;
public void submitJob(String jobName) {
lock.lock();
try {
currentJob++;
System.out.println("[" + jobName + "] 提交打印任务,任务编号: " + currentJob);
// 模拟打印耗时
Thread.sleep(100);
System.out.println("[" + jobName + "] 完成打印任务: " + currentJob);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
FairPrinterService service = new FairPrinterService();
// 创建多个线程提交任务
for (int i = 0; i < 5; i++) {
final String threadName = "Thread-" + i;
new Thread(() -> service.submitJob(threadName)).start();
}
}
}
运行结果你会发现,任务的提交顺序和执行顺序基本一致,这体现了公平锁的特性。
高级特性与最佳实践
公平锁 vs. 非公平锁
- 公平锁:
new ReentrantLock(true),线程严格按照请求锁的顺序获取锁,优点是避免了线程饥饿,缺点是牺牲了吞吐量,因为线程切换和上下文切换更频繁。 - 非公平锁:
new ReentrantLock(false)(默认),当一个线程释放锁时,新来的线程会和等待队列中的线程竞争锁,优点是吞吐量高,缺点是可能导致某些线程长时间获取不到锁(饥饿)。
建议:在大多数情况下,非公平锁是更好的选择,因为更高的吞吐量通常比绝对的公平性更重要。
注意事项
- 死锁:和
synchronized一样,忘记unlock()会导致死锁。永远在finally块中释放锁! - 锁的粒度:尽量只锁定必要的代码段,减小临界区的范围,以提高并发性能。
- 与
synchronized混用:绝对不要在一个锁对象上同时使用synchronized和ReentrantLock,这会导致不可预期的行为。
ReentrantLock 是 Java 并发包中一颗璀璨的明珠,它为 synchronized 提供了强大而灵活的补充,通过本文的深入剖析,我们了解到:
- ReentrantLock 提供了
tryLock、lockInterruptibly、公平锁和Condition等高级功能,解决了synchronized无法应对的复杂场景。 - 在选择锁机制时,应优先考虑
synchronized的简洁性,仅在需要其高级特性时才使用ReentrantLock。 - 正确、合理地使用 ReentrantLock,能够显著提升我们构建高性能、高并发应用的能力。
掌握 ReentrantLock,意味着你从“会用”并发工具,向“精通”并发编程迈出了坚实的一步,希望这篇文章能成为你并发征途中的有力导航,就去你的项目中尝试使用它吧!
