ReentrantLock 与 Condition:公平/非公平、可中断/超时与最佳实践

如果说 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/false
  • tryLock(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.park
  • AbstractQueuedSynchronizer.acquire
  • ConditionObject.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
相关推荐
m0_518019482 小时前
使用Seaborn绘制统计图形:更美更简单
jvm·数据库·python
2401_831824963 小时前
为你的Python脚本添加图形界面(GUI)
jvm·数据库·python
2401_879693873 小时前
用Pygame开发你的第一个小游戏
jvm·数据库·python
xushichao19893 小时前
实战:用OpenCV和Python进行人脸识别
jvm·数据库·python
2501_945423543 小时前
工具、测试与部署
jvm·数据库·python
Oueii3 小时前
数据分析师的Python工具箱
jvm·数据库·python
weixin_421922693 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
Liu628884 小时前
如何为开源Python项目做贡献?
jvm·数据库·python