ReentrantLock 完整详解
一、基础介绍
ReentrantLock 位于 java.util.concurrent.locks,是 JDK 提供的显式锁,替代传统 synchronized 隐式同步锁。
核心特性:可重入
同一线程可多次获取同一把锁,不会死锁;获取多少次,释放多少次。
synchronized 底层也是可重入锁,但 ReentrantLock 功能更强、更灵活。
核心对比 synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁类型 | 隐式锁,自动释放 | 显式锁,必须手动 unlock() |
| 可重入 | 支持 | 支持 |
| 公平锁 | 仅非公平 | 公平/非公平可切换 |
| 可中断等待 | 不可中断 | lockInterruptibly() 支持中断 |
| 尝试获取锁 | 无 | tryLock() 非阻塞尝试 |
| 多条件等待 | 1个等待队列(wait/notify) | 多个 Condition 等待队列 |
| 性能 | JDK1.6后优化,差距很小 | 高并发场景灵活可控 |
| 语法 | 代码块/方法 | try-finally 标准写法 |
二、两种锁模式:公平锁 / 非公平锁
构造函数控制模式:
// 1. 默认:非公平锁(吞吐量更高)
ReentrantLock lock = new ReentrantLock();
// 2. 传入true:公平锁,按线程等待顺序获取锁
ReentrantLock fairLock = new ReentrantLock(true);
1. 非公平锁(默认,推荐)
- 线程抢锁不排队,新线程上来直接竞争;
- 优势:上下文切换少,吞吐量高;
- 缺点:可能存在线程饥饿(长期抢不到锁)。
2. 公平锁
- 严格按照线程等待FIFO顺序获取锁;
- 优势:无饥饿,公平;
- 缺点:大量上下文切换,并发性能差,极少使用。
三、核心API方法
1. 获取锁
void lock()
阻塞获取锁,拿不到锁则无限等待,不响应中断。void lockInterruptibly() throws InterruptedException
阻塞等待锁,等待过程中线程被interrupt()中断,直接抛出异常,可处理中断。boolean tryLock()
非阻塞尝试,立刻返回:拿到锁true,没拿到false,不等待。boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
限时等待锁:指定时间内没拿到返回false;等待中可被中断。
2. 释放锁
void unlock()
- 必须在
finally执行,保证锁一定释放; - 可重入场景:加锁n次,必须释放n次,否则锁永久占用,死锁。
3. 可重入相关工具方法
// 当前线程持有锁次数
int getHoldCount()
// 判断当前线程是否持有锁
boolean isHeldByCurrentThread()
// 判断锁是否被任意线程占用
boolean isLocked()
// 公平锁:返回true是公平锁
boolean isFair()
// 返回等待该锁的线程数量(仅调试)
int getQueueLength()
4. 多条件等待 Condition
Condition newCondition()
一把锁可以创建多个等待队列,精准唤醒指定线程,比 wait/notify 强大很多。
condition.await():释放锁,阻塞等待;condition.signal():唤醒一个等待线程;condition.signalAll():唤醒全部。
四、标准使用模板(必看,防止死锁)
基础模板(lock())
ReentrantLock lock = new ReentrantLock();
public void test() {
// 1. 获取锁
lock.lock();
try {
// 同步业务逻辑
System.out.println("执行业务");
} finally {
// 2. 必须finally释放,异常也能解锁
lock.unlock();
}
}
可中断锁 lockInterruptibly()
public void interruptTest() throws InterruptedException {
lock.lockInterruptibly();
try {
// 业务
} finally {
lock.unlock();
}
}
限时尝试锁 tryLock
public void tryLockTest() {
// 等待2秒
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
// 获取锁成功,执行业务
} finally {
lock.unlock();
}
} else {
// 2秒没拿到锁,降级处理
System.out.println("获取锁失败,稍后重试");
}
}
五、核心特性代码演示
1. 可重入特性演示
同一线程多次加锁,计数累加,释放次数匹配才能完全解锁:
ReentrantLock lock = new ReentrantLock();
public void reentrantDemo() {
lock.lock();
try {
System.out.println("第1次持有次数:" + lock.getHoldCount()); // 1
innerMethod();
} finally {
lock.unlock();
System.out.println("释放1次,持有次数:" + lock.getHoldCount()); // 0
}
}
private void innerMethod() {
lock.lock();
try {
System.out.println("第2次持有次数:" + lock.getHoldCount()); // 2
} finally {
lock.unlock();
System.out.println("释放1次,持有次数:" + lock.getHoldCount()); // 1
}
}
错误场景:只lock两次,只unlock一次 → 锁永远占用,其他线程永久阻塞。
2. Condition 多条件精准等待(生产者消费者)
synchronized 只有一个等待队列,notify会随机唤醒;Condition可分区等待。
ReentrantLock lock = new ReentrantLock();
// 两个独立等待队列
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
List<String> queue = new ArrayList<>();
int maxSize = 5;
// 生产者
public void produce(String msg) throws InterruptedException {
lock.lock();
try {
// 队列满,生产者等待
while (queue.size() >= maxSize) {
notFull.await();
}
queue.add(msg);
System.out.println("生产:" + msg);
// 唤醒消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 消费者
public String consume() throws InterruptedException {
lock.lock();
try {
// 队列为空,消费者等待
while (queue.isEmpty()) {
notEmpty.await();
}
String msg = queue.remove(0);
System.out.println("消费:" + msg);
// 唤醒生产者
notFull.signal();
return msg;
} finally {
lock.unlock();
}
}
六、底层原理(AQS 核心)
ReentrantLock 底层基于 AQS(AbstractQueuedSynchronizer) 实现同步:
- state 同步状态:int变量
- state=0:无线程持有锁;
- state>0:锁被占用,数值=可重入次数;
- 持有线程标记:记录当前独占锁的线程,实现可重入;
- CLH双向阻塞队列:抢锁失败的线程封装成Node,排队阻塞;
- 公平/非公平差异:
- 非公平:线程上来直接CAS抢state,不管队列;
- 公平:先判断队列是否有等待线程,有则排队,不抢占。
加锁简易流程(非公平lock())
- CAS尝试把 state 从0改为1,成功 → 获取锁,设置当前持有线程;
- CAS失败:判断当前线程是否是持有锁线程,是则 state+1(可重入);
- 都不满足:当前线程封装Node加入阻塞队列,自旋+park阻塞等待唤醒;
- 解锁:state-1,state归0后,唤醒队列头部等待线程。
七、常见坑 & 注意事项
- unlock() 必须放在finally
业务代码抛出异常,不走finally会导致锁永不释放,其他线程永久阻塞。 - 不能跨线程释放锁
A线程获取锁,B线程调用unlock直接抛IllegalMonitorStateException。 - 可重入必须成对 lock/unlock
lock n次,必须 unlock n次,否则锁不会释放。 - await() 必须在锁内执行
Condition等待方法必须在lock()~unlock()之间调用,否则报错。 - while循环判断等待条件,不要用if
线程唤醒后可能被其他线程抢占资源,if会出现数据错乱,while循环重新校验。 - 公平锁性能差,业务极少使用
只有强顺序要求场景才考虑公平锁,绝大多数业务默认非公平。 - tryLock 不要写死循环自旋
无限while(tryLock())会疯狂占用CPU,建议加睡眠时间或超时时间。
八、适用场景
- 需要灵活中断等待锁的线程;
- 需要限时获取锁,避免无限阻塞;
- 需要精准分区等待(多Condition,生产者消费者、读写分离等);
- 需要控制锁公平性;
- 需要动态判断当前线程是否持有锁、获取重入次数等监控;
- 高并发复杂同步场景,
synchronized功能不足时。
九、同名任务串行执行拓展
同名任务串行、不同任务互不干扰,可以用 ConcurrentHashMap<String, ReentrantLock> 分组锁实现:
// key:任务分组名,value:分组独立可重入锁
private final Map<String, ReentrantLock> groupLockMap = new ConcurrentHashMap<>();
public void runTask(String group) {
ReentrantLock lock = groupLockMap.computeIfAbsent(group, k -> new ReentrantLock());
lock.lock();
try {
// 同组任务串行执行,其他分组互不影响
System.out.println("执行分组任务:" + group);
} finally {
lock.unlock();
}
}
优点:轻量、无额外线程池开销;缺点:大批量任务会阻塞调用线程,适合短时任务;长耗时任务推荐之前的分组单线程池方案。