一、ReentrantLock 是什么?
ReentrantLock 是 Java.util.concurrent.locks 包下的可重入独占锁,从字面意思拆解:
- Reentrant(可重入):同一个线程可以多次获取同一把锁,不会因为自己持有锁而阻塞(比如递归调用中加锁不会死锁)。
- Lock(锁):它是对 synchronized 关键字的补充和增强,通过编程式的方式实现锁的获取与释放。
1. 核心特性
- 可重入:和 synchronized 一样,支持同一线程重复加锁,锁会记录 "加锁次数",释放次数需和加锁次数一致才会真正释放。
- 显式锁 :必须手动调用
lock()获取锁,手动调用unlock()释放锁(通常放在 finally 块中,避免异常导致锁无法释放)。 - 公平 / 非公平锁 :
- 非公平锁(默认):线程获取锁时不按等待顺序,可能 "插队",性能更高(和 synchronized 一致)。
- 公平锁:线程按等待队列的顺序获取锁,避免 "饥饿",但性能略低(通过构造函数
new ReentrantLock(true)开启)。
- 可中断 :支持通过
lockInterruptibly()响应线程中断,避免线程无限等待锁。 - 超时获取 :支持
tryLock(long time, TimeUnit unit),在指定时间内获取不到锁则返回 false,不会永久阻塞。
2. 基础使用示例
java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
// 创建非公平锁(默认),公平锁:new ReentrantLock(true)
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
// 1. 获取锁
lock.lock();
try {
// 业务逻辑:临界区代码
System.out.println(Thread.currentThread().getName() + " 执行临界区代码");
// 可重入:同一线程再次获取锁
doInnerThing();
} finally {
// 2. 释放锁(必须放finally,避免异常导致锁泄漏)
lock.unlock();
}
}
private void doInnerThing() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 执行内部临界区代码");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
new Thread(demo::doSomething, "线程1").start();
new Thread(demo::doSomething, "线程2").start();
}
}
执行结果(非公平):
线程1 执行临界区代码
线程1 执行内部临界区代码
线程2 执行临界区代码
线程2 执行内部临界区代码
二、ReentrantLock 与 synchronized 的核心区别
我整理了一张核心维度的对比表:
| 对比维度 | ReentrantLock | synchronized |
|---|---|---|
| 锁的类型 | 显式锁(JDK层面)(手动获取 / 释放) | 隐式锁(JVM 自动获取 / 释放) |
| 可重入性 | 支持(可重入锁) | 支持(可重入锁) |
| 公平 / 非公平锁 | 支持(默认非公平,可手动指定公平) | 仅非公平锁 |
| 中断性 | 支持(lockInterruptibly ()) | 不支持(等待锁的线程无法被中断) |
| 超时获取锁 | 支持(tryLock (long time, TimeUnit)) | 不支持(要么获取锁,要么一直阻塞) |
| 条件变量(Condition) | 支持多个 Condition,可精准唤醒线程 | 仅支持一个,只能随机 / 全部唤醒线程 |
| 锁状态查询 | 支持(isLocked ()、getHoldCount () 等) | 不支持(无法主动查询锁状态) |
| 性能 | 高并发下性能更优(JDK1.6 后差距缩小) | 低并发下更简洁,JVM 优化充分 |
| 异常处理 | 必须手动释放(finally 块),否则锁泄漏 | 异常时自动释放锁,无需手动处理 |
关键区别详解
1. 显式 vs 隐式
- synchronized:写在方法 / 代码块上,JVM 在进入代码块时自动加锁,退出(正常 / 异常)时自动释放锁,无需手动操作。
- ReentrantLock:必须手动调用
lock()加锁,且必须在finally中调用unlock()释放(否则线程异常会导致锁永远无法释放,即 "锁泄漏")。
2. 公平锁支持
- synchronized:永远是非公平的,线程获取锁时不按等待顺序,可能后到的线程先拿到锁。
- ReentrantLock:默认非公平,但可以通过构造函数
new ReentrantLock(true)开启公平锁,保证等待最久的线程先获取锁(适合对顺序要求高的场景)。
3. 中断与超时获取
这是 ReentrantLock 最核心的优势之一:
java
// 示例:超时获取锁
public void tryLockWithTimeout() {
try {
// 尝试在1秒内获取锁,获取到返回true,否则返回false
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 获取到锁");
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 1秒内未获取到锁,放弃等待");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
}
}
synchronized 无法实现这种 "超时放弃" 的逻辑,只能一直阻塞。
4. 条件变量(Condition)
ReentrantLock 可以通过 newCondition() 创建多个 Condition,实现精准唤醒线程:
javascript
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
private final ReentrantLock lock = new ReentrantLock();
// 创建两个条件变量:等待读、等待写
private final Condition readCondition = lock.newCondition();
private final Condition writeCondition = lock.newCondition();
public void waitForRead() throws InterruptedException {
lock.lock();
try {
readCondition.await(); // 读线程等待
System.out.println("读线程被唤醒");
} finally {
lock.unlock();
}
}
public void signalRead() {
lock.lock();
try {
readCondition.signal(); // 精准唤醒读线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionDemo demo = new ConditionDemo();
new Thread(demo::waitForRead, "读线程1").start();
Thread.sleep(1000);
demo.signalRead(); // 唤醒读线程
}
}
而 synchronized 只能通过 wait()/notify()/notifyAll() 唤醒,notify() 是随机唤醒一个等待线程,notifyAll() 是唤醒所有,无法精准唤醒。
三、使用场景选择
- 优先用 synchronized:低并发场景、简单的临界区控制,代码更简洁,JVM 优化(偏向锁、轻量级锁)更充分,不易出错。
- 选择 ReentrantLock :
- 需要公平锁、可中断锁、超时获取锁的场景;
- 需要精准唤醒线程(多 Condition);
- 高并发场景下需要更灵活的锁控制(如手动查询锁状态)。
总结
- ReentrantLock 是可重入的显式锁,支持公平 / 非公平、中断、超时获取、多 Condition 等高级特性,需手动加锁 / 释放(务必放 finally);
- synchronized 是隐式锁,JVM 自动管理,简洁易用,低并发下更友好,但功能单一;
- 核心区别在于灵活性和可控性:ReentrantLock 可控性更强,synchronized 更简洁、容错率更高,实际开发中需根据场景选择。