ReentrantLock
ReentrantLock 是通过方法调用来加锁和解锁的可重入互斥锁。它提供与 synchronized 相近的互斥和可见性保证。本文从获取与释放锁入手,再说明公平策略,最后讲 Condition 的等待、通知和超时预算。
1. 面试核心
先记住这几条,再读正文- 锁的能力。
ReentrantLock在提供互斥和可见性的同时,支持可中断获取、超时获取和多个条件队列。 - 配对释放。 线程每次成功获取
ReentrantLock,都要在对应的finally中调用一次unlock()。 - 公平性。 公平锁优先让等待最久的线程获取锁,但无超时
tryLock()仍允许插队。卡 · 可中断获取与超时获取 - 条件等待。
Condition.await()会完全释放关联锁,返回前重新获取并恢复持有次数,线程随后需重查条件。卡 · 条件重查与通知时机 - 超时预算。 带时限的条件等待每轮都使用剩余预算,避免反复唤醒重新计时。卡 · 条件等待的剩余预算
2. 加锁与释放
显式锁由代码负责释放,所以正常返回和异常退出都要经过 unlock()。把获取锁放在 try 之前,再在 finally 中释放,可以让两者正确配对。
private final ReentrantLock lock = new ReentrantLock();
void update() {
lock.lock();
try {
updateState();
} finally {
lock.unlock();
}
}
示例只有获取成功后才进入 try,因此 finally 释放的是当前线程已持有的锁。如果改用可中断获取,获取过程抛出异常时会直接跳过 try,避免错误地解锁。
同一线程可以重复获取同一把锁,这称为重入。每次获取都增加持有次数,每次 unlock() 减少一次。只有次数降到零,其他线程才能取得锁。
3. 可中断获取与超时获取
请求有截止时间时,等锁也需要计入预算。带超时的 tryLock 可以限制等待时间,并在超时后让调用方拒绝请求或执行降级逻辑。
if (lock.tryLock(200, TimeUnit.MILLISECONDS)) {
try {
updateState();
} finally {
lock.unlock();
}
} else {
rejectBusyRequest();
}
示例最多等待 200 毫秒。期限内获取成功就更新数据;超时返回 false,调用方据此拒绝繁忙请求。该方法还会通过 InterruptedException 响应中断,所在方法需要处理或声明此异常。
选择获取方式时,需要区分以下四种行为:
lock()持续等待获取锁,被中断后保留中断状态。lockInterruptibly()等待时可以响应中断,让调用方取消等锁。tryLock()立即返回获取结果,适合只尝试一次的操作。tryLock(timeout, unit)同时支持超时和中断,适合有等待期限的请求。
需要避免无限等待,或退出潜在的锁循环等待时,可以使用后两类可取消的等待方式。退出后仍要释放此前已取得的资源。
4. 公平锁与非公平锁
公平策略决定锁空闲时,排队线程和新到线程怎样竞争。new ReentrantLock(true) 启用公平策略,竞争时优先让等待最久的线程获取锁。当前持有者重入时,仍可直接成功。
默认的非公平锁允许运行中的线程插队。它可以直接利用刚释放的锁,减少等待唤醒和线程切换的空档。公平锁需要把机会留给排队者,而排队者还要等待调度。所以,持续竞争时,公平锁通常吞吐更低,但各线程等待时间的差异更小。
无超时 tryLock() 会忽略公平策略,只要锁可用就能成功。需要遵守公平顺序的立即尝试,可以用 tryLock(0, unit)。后者还会响应中断。
公平性约束的是获取锁的顺序,操作系统仍独立决定何时调度线程。业务需要控制长期等待时,再考虑启用公平策略,并比较吞吐量和等待时间分布。
5. Condition 等待
Condition 是与锁关联的条件变量,让线程等待某个业务条件成立。比如,消费者发现队列为空时,可以释放锁并等待,给生产者加入数据的机会。
private final Condition notEmpty = lock.newCondition();
Item take() throws InterruptedException {
lock.lockInterruptibly();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
return queue.removeFirst();
} finally {
lock.unlock();
}
}
示例先持锁检查队列,再调用 await()。它把释放关联锁和进入等待作为一个协调好的动作,避免生产者的通知落在检查与等待的空档中。返回前,消费者会重新获取锁,并恢复调用前的持有次数。
恢复持锁后,要用 while 再检查队列。线程可以在没有收到有效通知时唤醒,这称为虚假唤醒。此外,其他消费者也能在它重新获取锁之前取走数据。所以,只有确认队列非空,才执行删除。
await() 只释放关联锁,当前线程持有的其他锁仍然保留。如果生产者需要这些锁才能加入数据,双方就会互相等待。因此,设计时还要检查等待者持有的其他锁。
6. signal 与 signalAll
生产者改变共享状态后,需要通知等待该状态的线程。更新和通知都在同一把锁内完成,消费者才能在重新获取锁后看到更新。
lock.lock();
try {
queue.addLast(item);
notEmpty.signal();
} finally {
lock.unlock();
}
示例先加入数据,再通知等待者。收到通知的消费者进入获取锁的队列,生产者仍持有锁,直到执行 unlock()。所以,消费者要重新获取锁后,才能从 await() 返回。
能确认唤醒一个等待者就足够时,使用 signal()。多个等待者依赖的条件较复杂,或者容易遗漏应被唤醒的线程时,使用 signalAll()。它会通知全部等待者,各线程取得锁后仍要重新检查条件。
也可以用不同的 Condition 区分不同业务条件。比如,生产者等待“队列未满”,消费者等待“队列非空”,分别通知能减少无关唤醒。
7. 条件重查与通知时机
收到通知后,线程仍要重新判断业务条件,因为通知到恢复执行之间,数据可以再次变化。下面用两个消费者说明 while 的作用:
- 两个消费者都发现队列为空,于是进入等待。
- 生产者加入一条数据,并通知两个消费者。
- 第一个消费者取得锁,取走唯一的数据。
- 第二个消费者取得锁时,队列已经再次变空,需要继续等待。
如果只用 if 在等待前检查一次,第二个消费者就会直接操作空队列。while 则在每次恢复持锁后重新判断,也能处理虚假唤醒。
8. 条件等待的剩余预算
条件一直未满足时,反复唤醒也应共用同一份等待预算。awaitNanos 会返回估算的剩余纳秒数,下一轮继续使用这个值:
long remaining = timeoutNanos;
while (queue.isEmpty()) {
if (remaining <= 0L) {
return false;
}
remaining = notEmpty.awaitNanos(remaining);
}
return true;
本片段在持锁的 try 内执行,外围由 finally 解锁。若每轮都传入原始超时,等待预算就会重新开始,反复唤醒会让总等待时间越拖越长。
如果请求期限还包含获取锁的时间,就要从调用入口计算统一截止时间。每次加锁或等待前,都用截止时间减去当前时间,得到剩余预算。
9. 常见问题
9.1 ReentrantLock 一定比 synchronized 快吗?
两者的快慢取决于 JVM、竞争强度和临界区耗时,现代 JVM 会优化它们的常见路径。只需要互斥和可见性时,可以优先选语义简单的 synchronized。需要超时、可中断获取、公平策略或多个条件队列时,再选 ReentrantLock,并用基准测试判断性能。
9.2 Condition 可以脱离关联锁使用吗?
ReentrantLock 创建的 Condition 要在持有对应锁时使用。调用 await、signal 或 signalAll 时缺少锁所有权,会抛出 IllegalMonitorStateException。
条件检查也要放在同一把锁内。否则,生产者会在消费者检查之后、开始等待之前发出通知。通知发出时尚无等待者,消费者随后进入等待,就错过了这次状态变化。
10 · 面试题卡片
10.1 公平锁为什么通常吞吐更低
出现公司:蚂蚁集团、美团
考察重点
- 解释插队为什么能减少锁交接时的调度等待。
- 区分锁的排队顺序与操作系统调度线程的顺序。
- 说明无超时
tryLock()为什么仍可在公平锁上插队。
相关内容:可中断获取与超时获取和公平锁与非公平锁。
参考回答
公平锁要把空闲锁留给排队者,因此会增加等待唤醒和调度的交接时间。
非公平锁允许运行中的线程直接取得刚释放的锁,减少这段空档。持续竞争时,公平锁通常吞吐更低,但线程之间的等待时间差异更小。
锁的公平策略仍受操作系统调度影响,无超时 tryLock() 也允许插队。因此,应在需要限制长期等待时使用公平锁,并同时测量吞吐量和等待时间分布。
10.2 Condition.await 为什么必须放在 while 循环中?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:解释虚假唤醒和其他线程抢先操作,为什么都要求重新检查条件。
相关内容:条件重查与通知时机。
参考回答
await() 返回时,业务条件仍可能为假,所以必须用 while 再次检查。
线程可以虚假唤醒,也可以在重新获取锁前被其他消费者抢先取走数据。循环让线程只在条件成立时继续,否则再次释放锁并等待。
10.3 条件被反复唤醒时,怎样避免等待超出原定预算?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:说明怎样使用 awaitNanos 的剩余时间,以及何时需要统一截止时间。
相关内容:条件等待的剩余预算。
参考回答
每轮等待都使用剩余预算,预算耗尽后返回失败。
awaitNanos 返回估算的剩余时间,可以直接传给下一轮等待。如果每轮都重新传入原始时长,反复唤醒就会延长总等待。
请求还要限制加锁时间时,应从入口计算统一截止时间,再为每次等待计算剩余预算。