Java并发面试总把 synchronized 和 ReentrantLock 混在一起怎么办?4个场景讲清锁、条件队列和中断

Java并发面试总把 synchronized 和 ReentrantLock 混在一起怎么办?4个场景讲清锁、条件队列和中断

synchronized 与 ReentrantLock 不该靠背关键词二选一,本文用 4 个面试场景讲清可重入性、超时与中断获取、Condition 条件队列和公平锁边界,帮你把 API 区别答成并发设计判断。

先把 30 秒回答说完整

如果面试官问「synchronized 和 ReentrantLock 有什么区别」,不要一上来背「一个自动释放,一个手动释放」。更稳的回答顺序是:
两者都能做互斥,ReentrantLock 和 synchronized 的基本行为与语义一致,也都支持同一线程重入。synchronized 使用对象的隐式监视器,锁的获取和释放受代码块范围管理;ReentrantLock 需要显式调用 lock 和 unlock,但因此可以增加 tryLock、可中断获取、超时等待、公平策略和多个 Condition。选择时先看业务是否需要这些控制能力,不要直接下结论说谁天然更快。
Lock 接口文档明确说明,成功的 lock/unlock 操作需要提供与内置监视器锁相同的内存同步语义;它与 synchronized 的主要差别,是提供更灵活的加锁结构和额外的获取方式。1
下面用 4 个面试场景,把这段话落到可以判断的细节上。

场景一:只保护一小段临界区,为什么不直接用 synchronized?

假设你只是给一个共享计数器加锁:
public synchronized void increase() {
    count++;
}
或者:
private final Object monitor = new Object();

public void increase() {
    synchronized (monitor) {
        count++;
    }
}
这类代码的优点是边界直观。方法或代码块结束后,监视器锁会按结构释放,异常路径也不需要额外写释放动作。Oracle 的 Lock 文档把这种特征称为 block-structured locking:锁的获取和释放都落在明确的代码范围里,也因此更容易避免忘记释放的错误。1
如果改成 ReentrantLock,规范写法必须把 unlock() 放进 finally
private final ReentrantLock lock = new ReentrantLock();

public void increase() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}
ReentrantLock 的官方文档也直接给出了这种 before/after 结构。它的价值不是让每个计数器都换一种写法,而是当你确实需要「怎么等、等多久、等不到怎么办」时,提供更细的控制。2
面试里可以这样收束:
  • 临界区简单、没有取消或超时要求,synchronized 往往更省心。
  • 需要把加锁和释放拆到不同控制路径,或者需要尝试、限时和可中断获取,再考虑 ReentrantLock
  • 不能把「代码更长」误答成「性能更好」。两者的取舍首先是控制能力和出错风险。

场景二:线程不能无限等锁,怎么答 tryLock 和中断?

lock() 获取不到锁时会继续等待。若业务有时间预算,面试官可能会追问:线程一直排队,超时怎么办?
这时可以用限时 tryLock
if (lock.tryLock(200, TimeUnit.MILLISECONDS)) {
    try {
        updateInventory();
    } finally {
        lock.unlock();
    }
} else {
    recordTimeout();
}
tryLock(long, TimeUnit) 在规定时间内拿到锁就返回 true,等待时间耗尽则返回 false;等待期间被中断,会抛出 InterruptedException。无参 tryLock() 则是不等待的立即尝试。1
如果需求是「排队等锁,但允许取消」,可以回答 lockInterruptibly()
try {
    lock.lockInterruptibly();
    try {
        callRemoteService();
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}
这里有两个容易丢分的点:
  1. tryLock() 不是「等一会儿」。无参版本拿不到就立即返回 false,限时版本才会等待。
  2. 捕获 InterruptedException 后,不要静默吞掉中断。示例里恢复中断标记,是为了让上层仍能看到取消信号;具体是返回、重试还是结束任务,要按业务语义决定。
Oracle 文档把非阻塞尝试、可中断获取和限时获取都列为 Lock 相比 synchronized 的扩展能力。面试官真正想听的不是方法名清单,而是你能否把「超时」「取消」和「释放锁」放到同一条控制链里。1

场景三:为什么 Condition 要配合 while,而不是 if?

看一个有界队列。生产者在队列满时等待,消费者在队列空时等待。用一把锁配两个条件队列:
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();

public void put(Item item) throws InterruptedException {
    lock.lockInterruptibly();
    try {
        while (queue.isFull()) {
            notFull.await();
        }
        queue.add(item);
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
}
这里的 while 不能换成 ifCondition.await() 允许虚假唤醒,线程被唤醒也不代表队列此刻一定有空间;线程重新拿到锁后,必须再次检查条件。Oracle 的 Condition 文档明确建议始终在循环中测试状态谓词,并说明 await() 会原子地释放关联锁,返回前重新获取这把锁。3
Condition 的实用区别是:同一把 Lock 可以关联多个等待集合。生产者只在 notFull 上等待,消费者只在 notEmpty 上等待,状态改变后再唤醒对应一侧。它不是一个脱离锁独立工作的「通知器」,创建它要用 lock.newCondition(),调用 await()signal()signalAll() 也要遵守关联锁的使用约束。3
面试时可以顺手把 synchronized 的对应关系说清楚:
隐式监视器显式锁
synchronizedlock.lock() / unlock()
wait()condition.await()
notify() / notifyAll()signal() / signalAll()
一个对象的监视器等待集合可以给同一把锁建立多个 Condition
但这不是把两套 API 的名字机械替换。Object.wait() 要求当前线程先拥有对象监视器,等待时释放该对象上的同步占有,并且同样可能因通知、中断、超时或伪唤醒而返回,所以也要用 while 复查条件。4

场景四:公平锁是不是更公平?tryLock 会不会插队?

new ReentrantLock(true) 表示公平策略。竞争时,它倾向于把锁交给等待时间更长的线程;官方文档同时提醒,公平锁通常会牺牲一部分吞吐量,而且锁的公平不等于线程调度公平。
更容易被追问的是这句:
ReentrantLock fairLock = new ReentrantLock(true);
boolean acquired = fairLock.tryLock();
即便锁配置成公平,无参 tryLock() 也不遵守公平顺序。只要锁当下可用,它就可能直接拿到锁,即使已有线程在排队。要把这个边界说出来,面试官才知道你不是只记住了构造器里的 true2
可以用这张表快速判断:
需求更应该讲什么不要怎么答
普通互斥、临界区短synchronized 的结构清楚、释放风险低「ReentrantLock 一定更快」
等锁要超时tryLock(timeout),超时走降级或返回把无参 tryLock() 说成限时等待
等锁要能取消lockInterruptibly() 与中断处理捕获异常后什么都不做
多种状态分别等待一把锁配多个 Condition,用 while 复查await() 外只写 if
竞争顺序有要求公平策略的吞吐与等待时间取舍说公平锁能保证线程轮流执行

最后用一段项目话术收尾

我不会先按 API 名称二选一,而是先看锁的需求。如果只是保护一个边界清楚的临界区,我优先考虑 synchronized,减少手动释放带来的风险。如果需要超时、可中断获取,或者同一把锁下有多个独立等待条件,我会用 ReentrantLock 和对应的 Condition。无论选哪种方案,我都会确认锁的持有范围、异常路径、等待条件和监控信号,避免只看代码能不能编译。
这段话还有一个好处:它把「会背区别」变成了「会做选择」。下一次被追问时,先回答业务约束,再落到 API,通常比背一串方法名更稳。

面试前自查清单

  • 能否用一句话说明两者都提供互斥和可重入的基本语义?
  • 能否解释为什么 ReentrantLockunlock() 要放进 finally
  • 能否区分立即尝试、限时等待和可中断获取?
  • 能否说清 await() 释放锁、返回前重新获取锁,以及为什么必须循环检查条件?
  • 能否举出同一把锁配两个 Condition 的场景?
  • 能否说明公平锁不保证线程调度公平,以及无参 tryLock() 可能插队?
  • 能否根据自己的项目说出锁的保护对象、等待条件、超时处理和异常路径?
如果这些问题都能结合一个真实项目回答,面试官追问的重点就会从「你记没记住区别」转向「你能不能把并发边界设计清楚」。

Related content

  • Sign in to comment.
More from this channel