Java面试之synchronized与Lock的区别

synchronized不能中断等待线程,因其底层monitor不响应interrupt信号;而ReentrantLock的lockInterruptibly()可响应中断并抛出InterruptedException。

为什么 synchronized 不能中断等待线程

synchronized 是 JVM 层的内置锁,进入阻塞状态后线程会挂起,且无法被 Thread.interrupt() 中断。这是由底层 monitor 实现决定的——它不响应中断信号,只会一直等锁释放。

Lock 接口(如 ReentrantLock)提供了 lockInterruptibly() 方法,调用时若线程被中断,会抛出 InterruptedException 并立即退出等待队列。

  • 使用 synchronized 时,即使调用 thread.interrupt(),线程仍卡在 monitorenter 指令上,无法响应
  • ReentrantLock.lockInterruptibly() 在获取锁前会检查中断状态,适合需要超时或可取消的场景(如任务调度、RPC 调用)
  • 注意:lockInterruptibly() 不是“自动中断”,必须配合外部中断逻辑(比如另一个线程调用 interrupt()

tryLock() 和 wait/notify 不能混用

synchronized 块内可以安全使用 Object.wait() / notify(),因为它们绑定的是同一个 monitor 对象;但 Lock 不提供原生的等待/唤醒机制,必须搭配 Condition 使用。

直接在 ReentrantLock 保护的代码里调用 wait() 会抛 IllegalMonitorStateException,因为此时线程并未持有该对象的 monitor 锁。

  • synchronized(obj) { obj.wait();

    }
    合法,obj 就是 monitor
  • lock.lock(); obj.wait(); 非法,obj 和锁无关
  • 正确方式是:Condition condition = lock.newCondition(); condition.await(); condition.signal();
  • 一个 Lock 可创建多个 Condition,实现更精细的等待队列(比如读写锁中分别管理读等待和写等待)

公平性与性能差异在哪体现

synchronized 始终是非公平的,且无法配置;ReentrantLock 默认也是非公平,但构造时传 true 可启用公平模式:new ReentrantLock(true)

公平锁会按线程入队顺序分配锁,避免饥饿,但吞吐量明显下降——每次加锁都要遍历同步队列确认头节点,还可能引发频繁的线程挂起/唤醒切换。

  • 高并发争抢下,非公平锁平均性能比公平锁高 2–3 倍(JDK8 测试数据)
  • synchronized 经过 JIT 优化后,在无竞争或轻度竞争时几乎无额外开销;而 ReentrantLock 即使未争抢,也有对象创建和方法调用成本
  • 公平锁只应在明确存在饥饿风险(如定时任务线程长期拿不到锁)时启用,不是“更安全”的默认选项

锁升级失败会导致 synchronized 退化为重量级锁

JVM 对 synchronized 做了多层优化:偏向锁 → 轻量级锁 → 重量级锁。一旦发生锁竞争(比如两个线程同时进入同一把锁),偏向锁会被撤销,后续再进入就会直接走轻量级锁路径;若自旋失败(默认 10 次),就膨胀为重量级锁,线程进入操作系统 Mutex 等待。

这个过程不可逆,且撤销偏向锁需全局安全点(stop-the-world),可能造成明显停顿。

  • ReentrantLock 没有这种“锁升级”概念,始终基于 AQS 队列,行为稳定可预期
  • 可通过 JVM 参数关闭偏向锁(-XX:-UseBiasedLocking)来规避撤销开销,尤其在容器环境或短生命周期应用中
  • 注意:JDK15+ 默认禁用偏向锁,JDK17 彻底移除,所以新项目基本不用考虑这部分复杂性
public class LockVsSyncExample {
    private final Object syncObj = new Object();
    private final ReentrantLock lock = new ReentrantLock();

    public void syncMethod() {
        synchronized (syncObj) {
            // 正确:wait 必须在 synchronized 块中
            try {
                syncObj.wait(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public void lockMethod() {
        lock.lock();
        try {
            // 错误:不能在这里调用 wait()
            // syncObj.wait(); // 抛 IllegalMonitorStateException

            // 正确:用 Condition
            Condition condition = lock.newCondition();
            condition.await(100, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}
锁的语义一致性比语法糖更重要。很多人以为 ReentrantLock 就是 synchronized 的升级版,其实它们适用边界很不同:前者适合需要中断、超时、多条件等待的显式控制场景;后者在大多数简单同步需求中更轻量、更不易出错。真正容易被忽略的是——synchronized 的 monitor 本质决定了它和 JVM 内存模型、GC 安全点、逃逸分析深度耦合,而这些细节在调试死锁或性能抖动时才突然变得关键。