面试专题

synchronized 和 ReentrantLock 的区别

对比 synchronized 与 ReentrantLock 的实现、可重入、公平锁、中断、超时、Condition 和适用场景。

难度:进阶更新:2026-07-21

synchronized和reentrantLock的区别?

面试标准回答

synchronized 是 JVM 层面的内置锁,语法简单,进入同步块后自动加锁,退出时自动释放,支持可重入和内存可见性。ReentrantLock 是基于 AQS 实现的显式锁,同样支持可重入,但提供了公平锁、可响应中断获取锁、tryLock、超时获取以及多个 Condition 等高级能力。两者在现代 JDK 中性能差距通常不是主要选型依据;功能简单时优先 synchronized,需要精细控制时使用 ReentrantLock,并且必须在 finally 中释放锁。

一句话记忆:

synchronized 简单自动,ReentrantLock 灵活可控。

详细讲解

synchronizedReentrantLock 都能实现互斥和可见性,但定位不同:

synchronized 是 JVM 原生关键字,语法简单;ReentrantLock 是 JUC 提供的显式锁,功能更丰富、控制能力更强。

一、基本用法

synchronized

synchronized (lock) {
    // 临界区
}

或者:

public synchronized void method() {
}

锁会自动释放。

ReentrantLock

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

必须手动释放,所以通常必须写在 finally 中。


二、核心区别

对比项synchronizedReentrantLock
实现层次JVM 关键字、MonitorJava 类,基于 AQS
加锁释放自动手动
可重入支持支持
公平锁不支持显式配置支持
可中断获取锁不支持lockInterruptibly()
尝试获取锁不支持tryLock()
超时获取锁不支持tryLock(timeout, unit)
条件队列一个 Monitor WaitSet可创建多个 Condition
等待通知wait/notify/notifyAllawait/signal/signalAll
锁状态查询能力有限提供较多查询方法
编码复杂度较高
异常释放自动释放必须 finally unlock

三、两者都支持可重入

可重入指:

同一个线程已经持有锁时,可以再次获取同一把锁。

synchronized

public synchronized void methodA() {
    methodB();
}

public synchronized void methodB() {
}

同一个对象上的两个同步方法,线程可以重入。

ReentrantLock

lock.lock();
try {
    lock.lock();
    try {
        // 重入
    } finally {
        lock.unlock();
    }
} finally {
    lock.unlock();
}

获取几次,就必须释放几次。


四、ReentrantLock 支持公平锁

ReentrantLock fairLock = new ReentrantLock(true);

公平锁会尽量按 AQS 队列顺序获取锁。

默认是非公平锁:

ReentrantLock lock = new ReentrantLock();

非公平锁允许新线程插队,吞吐量通常更高。

synchronized 没有 API 让你指定公平性,通常按非公平竞争理解。


五、ReentrantLock 支持可响应中断等待

假设线程正在等待锁。

使用:

lock.lock();

即使线程被中断,也不会因为中断立即退出获取锁过程。

而:

lock.lockInterruptibly();

等待期间如果收到中断,会抛出:

InterruptedException

例如:

try {
    lock.lockInterruptibly();
    try {
        doWork();
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

这对于避免线程长时间死等、响应取消请求很有价值。


六、ReentrantLock 支持尝试获取和超时

立即尝试

if (lock.tryLock()) {
    try {
        doWork();
    } finally {
        lock.unlock();
    }
} else {
    // 获取失败,走降级逻辑
}

超时尝试

if (lock.tryLock(2, TimeUnit.SECONDS)) {
    try {
        doWork();
    } finally {
        lock.unlock();
    }
} else {
    // 两秒内未拿到锁
}

synchronized 一旦进入竞争,只能等待,无法直接设置超时。

这也是 ReentrantLock 在需要降级、超时控制时的重要优势。


七、Condition 比 wait/notify 更灵活

synchronized 的一个 Monitor 只有一个 WaitSet。

例如阻塞队列里:

生产者等待 notFull
消费者等待 notEmpty

使用 synchronized 时,生产者和消费者都在同一个 WaitSet 中,通常要:

notifyAll();

而 ReentrantLock 可以创建多个 Condition:

Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

生产者等待:

while (queue.size() == capacity) {
    notFull.await();
}

消费者等待:

while (queue.isEmpty()) {
    notEmpty.await();
}

生产完成:

notEmpty.signal();

消费完成:

notFull.signal();

这样可以精确唤醒,减少无效唤醒。


八、底层实现不同

synchronized

经典理解:

对象头 Mark Word
+
Monitor
+
JVM 锁优化

JDK 8 中可能经历:

偏向锁
→ 轻量级锁
→ 重量级锁

这是 HotSpot 的 JVM 实现优化。

ReentrantLock

主要基于:

AbstractQueuedSynchronizer

也就是 AQS。

核心状态:

volatile int state;

独占锁时:

state = 0  未加锁
state = 1  第一次获取
state > 1  重入次数

竞争失败的线程进入 CLH 变体同步队列,并通过 park/unpark 等待和唤醒。


九、异常情况下的差异

synchronized

synchronized (lock) {
    throw new RuntimeException();
}

离开同步块时,JVM 自动释放锁。

ReentrantLock:

lock.lock();
doWork();
lock.unlock();

如果 doWork() 抛异常:

unlock 没执行
→ 锁永久未释放

所以必须写:

lock.lock();
try {
    doWork();
} finally {
    lock.unlock();
}

这是 ReentrantLock 最常见的编码风险。


十、性能区别

早期 JDK 中,ReentrantLock 性能常常明显优于 synchronized。

但 JDK 6 以后,JVM 对 synchronized 做了大量优化:

  • 偏向锁
  • 轻量级锁
  • 自旋
  • 锁消除
  • 锁粗化

现代 JDK 中:

两者性能差异通常不是选型的首要依据。

应该根据功能需求选择,而不是简单认为 ReentrantLock 一定更快。


十一、什么时候使用 synchronized

适合:

  • 临界区简单
  • 不需要公平锁
  • 不需要超时获取
  • 不需要中断锁等待
  • 只有一个等待条件
  • 希望代码简单、自动释放锁

例如:

public synchronized void increment() {
    count++;
}

一般优先原则:

能用 synchronized 清晰表达时,优先用 synchronized。


十二、什么时候使用 ReentrantLock

适合:

  • 需要公平锁
  • 需要 tryLock
  • 需要超时获取锁
  • 需要可中断等待
  • 需要多个 Condition
  • 需要查询等待队列、持锁状态
  • 需要更精细的锁控制

例如转账时避免死锁:

if (accountA.lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        if (accountB.lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                transfer();
            } finally {
                accountB.lock.unlock();
            }
        }
    } finally {
        accountA.lock.unlock();
    }
}

使用 synchronized 很难实现这种超时退避。


十三、wait/notify 和 await/signal 对应关系

synchronizedReentrantLock
wait()Condition.await()
notify()Condition.signal()
notifyAll()Condition.signalAll()

两者都要求:

调用等待或通知方法前,必须先持有对应的锁。

并且等待都应该放在 while 中:

while (!conditionSatisfied()) {
    condition.await();
}

防止虚假唤醒和竞争后条件失效。


十四、常见误区

误区一:ReentrantLock 才支持可重入

不对。两者都可重入。

误区二:ReentrantLock 一定比 synchronized 快

不对。现代 JVM 下要看场景,功能差异比纯性能更重要。

误区三:synchronized 会自动释放,ReentrantLock 不会

更准确地说:

  • synchronized 离开同步块时自动释放
  • ReentrantLock 必须显式 unlock

误区四:公平锁一定更好

公平锁吞吐量通常更低,只在有明确公平性需求时使用。

误区五:tryLock 失败后可以直接 unlock

不可以。只有成功获取锁后才能释放。

正确写法:

boolean locked = false;
try {
    locked = lock.tryLock();
    if (!locked) {
        return;
    }

    doWork();
} finally {
    if (locked) {
        lock.unlock();
    }
}

相关主题

DISCUSSION

评论与讨论

留下你的想法