如果说 synchronized 是"语法级锁",那 ReentrantLock 更像"工程级锁":
- 你可以选公平/非公平
- 你可以可中断
- 你可以超时获取
- 你可以有多个条件队列(Condition)做精准唤醒
这篇按"从会用 -> 用对 -> 会排查"的主线写。
你可以把它当成对 synchronized 的"工程增强版":
synchronized:简单、语法级、基本够用ReentrantLock:可选公平、可中断、可超时、多条件队列
0. 和 synchronized 的对比表(面试最常用)
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 获取方式 | 语法关键字 | 显式 lock/unlock |
| 可中断 | 不支持(获取锁不可中断) | lockInterruptibly() |
| 超时 | 不支持 | tryLock(timeout) |
| 公平性 | 非公平为主 | 可选公平/非公平 |
| 条件队列 | 一个 monitor wait-set | 多个 Condition |
| 释放锁 | 自动(异常也会释放) | 必须 finally unlock |
1. 先给结论:什么时候用 ReentrantLock
优先用 synchronized 的场景:
- 临界区短
- 不需要超时/可中断/多条件队列
- 追求简单
考虑用 ReentrantLock 的场景:
- 需要
tryLock()/tryLock(timeout) - 需要
lockInterruptibly() - 需要多个 Condition(例如生产者/消费者两个条件)
- 需要更精细的监控与扩展
2. ReentrantLock 的核心能力清单
lock():获取锁(不可中断,直到拿到)unlock():释放锁tryLock():尝试获取,立即返回 true/falsetryLock(timeout):超时等待lockInterruptibly():可中断等待newCondition():创建条件队列
关键约束:
unlock()必须放在finally,否则异常会导致锁无法释放
3. 公平锁 vs 非公平锁:吞吐与延迟的权衡
3.1 非公平锁(默认)
特点:
- 允许"插队"抢锁
- 吞吐更高
- 但尾延迟可能抖动更大
适合:
- 高吞吐场景
3.2 公平锁
特点:
- 更倾向按等待队列顺序获取
- 吞吐可能下降(更多排队切换)
- 延迟更稳定
适合:
- 对公平性/延迟更敏感的场景
4. 可中断与超时:这才是工程里最常用的价值
4.1 lockInterruptibly():可中断等待
当线程在等待锁时,如果你希望它能响应中断并退出:
- 用
lockInterruptibly()
适用:
- 任务取消
- 线程池 shutdown
4.2 tryLock(timeout):避免无限等待
适用:
- 你不希望请求线程一直卡住
- 希望在超时后走降级/失败返回
5. Condition:精准等待/唤醒(对比 wait/notify)
Condition 的价值在于:
- 一个 Lock 可以创建多个条件队列
- 唤醒更精准,避免
notifyAll带来的惊群
5.1 基本用法模板
java
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
void put() throws InterruptedException {
lock.lock();
try {
while (/* full */) {
notFull.await();
}
// enqueue
notEmpty.signal();
} finally {
lock.unlock();
}
}
注意两条铁律:
await/signal必须在持锁情况下调用- 等待条件用
while,不用if(防止虚假唤醒)
6. 一个可运行的模板:双 Condition 的生产者/消费者
用 Condition 最经典的场景就是有界队列。
- 队列空:消费者等待
notEmpty - 队列满:生产者等待
notFull
java
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BoundedBuffer<T> {
private final Queue<T> q = new ArrayDeque<>();
private final int cap;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public BoundedBuffer(int cap) {
this.cap = cap;
}
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (q.size() == cap) {
notFull.await();
}
q.add(x);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (q.isEmpty()) {
notEmpty.await();
}
T v = q.remove();
notFull.signal();
return v;
} finally {
lock.unlock();
}
}
}
你在面试里可以强调两点:
await()会释放锁,唤醒后要重新竞争锁- 必须用
while防止虚假唤醒
6.1 await 会释放锁吗
会。
await()会释放当前锁并进入条件队列等待- 被
signal唤醒后,会重新竞争锁,拿到锁后才从await()返回
6.2 signal vs signalAll:什么时候选哪个
- signal:唤醒一个等待线程(更精准、开销更小)
- signalAll:唤醒所有等待线程(避免遗漏,但可能惊群)
工程选择建议:
- 能明确只需要唤醒一个消费者/生产者时用
signal - 条件变更可能让多个线程都满足时,再考虑
signalAll
7. 常见坑(非常高频)
- 忘记 finally unlock:最危险
- 用 if 代替 while:虚假唤醒导致条件不成立仍继续执行
- signal 位置不对:修改共享状态后再 signal
- signalAll 滥用:惊群导致性能下降
- 锁内做慢操作:RPC/IO/大循环导致锁占用时间过长
再补两个常见坑:
- 用错锁对象:await/signal 必须是同一个 lock 创建出来的 Condition
- 错误的唤醒顺序:先修改共享状态,再 signal
8. 线上排查:锁竞争/死锁怎么定位
8.1 线程状态
WAITING (parking):常见于 AQS/Lock 相关等待BLOCKED:常见于 synchronized monitor 竞争
8.2 jstack 观察点
你重点看:
- 是否大量线程卡在同一个业务方法
- 是否出现
java.util.concurrent.locks.AbstractQueuedSynchronizer相关栈
常见关键词:
java.util.concurrent.locks.LockSupport.parkAbstractQueuedSynchronizer.acquireConditionObject.await
8.3 死锁定位
jstack通常会直接打印 "Found one Java-level deadlock"- 或你看到线程互相持有对方需要的锁
9. 工程观测:ReentrantLock 自带的一些"可读指标"
在排查热点锁时,这些方法很有用(用于日志/指标上报):
lock.isFair():是否公平锁lock.hasQueuedThreads():是否有人在排队lock.getQueueLength():估算排队线程数
10. 面试追问 Q&A(高频)
- Q:为什么 Condition 要用 while?
- A:因为可能虚假唤醒,也可能被唤醒后条件仍不满足,while 能保证条件正确。
- Q:signal 后线程立刻执行吗?
- A:不会,signal 只是把线程从条件队列移到同步队列,最终还要重新竞争锁。
- Q:公平锁一定严格公平吗?
- A:它更倾向 FIFO,但仍有实现细节;工程上理解为"延迟更稳、吞吐略降"。
11. 面试表达(30 秒讲清楚)
- ReentrantLock 是基于 AQS 的可重入锁,比 synchronized 更灵活。
- 默认非公平,吞吐更高;公平锁更稳定但有额外排队开销。
- 它支持
tryLock、超时等待与lockInterruptibly可中断等待。 - Condition 相当于增强版 wait/notify,一个锁可以有多个条件队列,await 会释放锁,signal 后需要重新竞争锁。
- 排查锁竞争看线程 parking 与 AQS 栈,死锁用 jstack 定位互相等待。
12. 总结
- synchronized 简单好用;ReentrantLock 更工程化
- 需要超时/可中断/多条件队列 -> 选 ReentrantLock
- Condition 的正确姿势:持锁 + while + 修改状态后再 signal