← 返回题目列表

synchronized 和 ReentrantLock 有什么区别?

高频 中等 第 13 / 31 题 更新于 2026/07/25
并发AQS

简化版

synchronized 是 JVM 内置的监视器锁,写法简单、出块/异常时自动释放ReentrantLock 是基于 AQS 的 JDK 类锁,功能更强——支持可中断、超时获取、公平锁、多个条件队列,但必须在 finally 里手动 unlock()。没有特殊需求优先用 synchronized

详细版

两者都保护临界区(同一时刻只有一个线程进),也都可重入(同一线程能重复获取自己已持有的锁,不会自锁)。区别在功能和用法:

维度synchronizedReentrantLock
层面JVM 关键字、监视器锁JDK 类,底层基于 AQS
释放出同步块/抛异常自动释放必须显式 unlock()
等锁时可中断不支持lockInterruptibly() 支持
超时获取不支持tryLock(timeout) 支持
公平性只能非公平可选公平/非公平
条件队列一组 wait/notify一个锁可建多个 Condition
// synchronized:只锁真正要保护的那几行
synchronized (lock) {
    balance -= amount;
}

// ReentrantLock:finally 里 unlock 是「正确性要求」不是习惯
Lock lock = new ReentrantLock();
lock.lock();
try {
    updateSharedState();
} finally {
    lock.unlock();
}

完整版教学

一、为什么默认选 synchronized

「synchronized 慢、ReentrantLock 快」是过时结论。现代 HotSpot 会先走轻量级锁路径,竞争加剧时再膨胀为重量级监视器;偏向锁已在 JDK 15 默认禁用,并于后续版本移除。它最大的工程优势是自动释放:退出同步块或抛异常时,JVM 都会释放监视器。

反观 ReentrantLock,如果 unlock() 漏写或没有放进 finally,异常路径可能让锁长期得不到释放,后续竞争线程持续阻塞。所以能力越大责任越大——没用到 ReentrantLock 的高级功能,就不必额外承担手动管理风险。

二、ReentrantLock 到底解决哪些 synchronized 做不到的事

ReentrantLock 应该是「我需要某个具体能力」,而不是「它听起来更高级」:

  • 可中断等待 lockInterruptibly():线程在等锁时能响应中断、放弃等待——适合需要能取消的任务,避免死等。
  • 超时获取 tryLock(2, SECONDS):等不到就返回,可用来避免死锁(拿不到第二把锁就回退重试)。
  • 公平锁 new ReentrantLock(true):按排队顺序给锁,避免线程饥饿(代价是吞吐下降)。synchronized 只能非公平。
  • 多个条件队列 Condition:生产者-消费者里,可以分别建「队列非满」和「队列非空」两个 Condition,精确唤醒对应线程,而 synchronized 的 notifyAll 会把所有等待线程都吵醒。

三、两个必须知道的用法陷阱

tryLock 失败不能 unlock

if (lock.tryLock()) {
    try { ... } finally { lock.unlock(); }   // 只有拿到锁才 unlock
}
// tryLock 返回 false 时若也 unlock,会抛 IllegalMonitorStateException

② synchronized 的锁对象要私有且稳定

// 避免用 this、字符串常量或可被外部拿到的对象当锁,否则可能与无关代码竞争同一把锁
private final Object lock = new Object();   // 私有 final 专用锁对象

字符串常量在常量池里全局共享,用它当锁可能和完全不相干的代码撞锁,引发诡异的死锁或阻塞。

四、锁不是越多越安全

线程安全还取决于锁粒度临界区内容

  • 别把 RPC、数据库、磁盘 I/O 放进锁里——会把并发拖成串行,吞吐暴跌;
  • 但也别为了「锁得少」把一个必须整体完成的「校验+写入」拆到锁外,那会产生竞态。
  • 单变量累加优先用 AtomicLong(CAS 无锁);读多写少用 ReadWriteLock / StampedLock最稳的设计是减少共享可变状态,而不是无止境加锁。

五、内存语义相同,能力边界不同

对正确同步而言,释放 Lock 与随后获取同一把 Lock 具有与监视器解锁/加锁相同类型的内存同步效果。二者都能保护复合不变量,差别主要在控制能力与使用方式,而不是“一个可见、一个不可见”。

需求synchronizedReentrantLock
自动释放退出语句块自动完成必须 finally unlock()
可中断等锁进入监视器等待不可直接中断lockInterruptibly()
超时/尝试不提供 tryLock APItryLock()/带超时重载
多条件队列每个监视器一个 wait set可创建多个 Condition
公平策略无配置开关构造时可选公平锁

公平锁也不是严格实时调度保证,tryLock() 等路径还有各自语义。选择 API 前应先明确是否真正需要可中断、超时或多个条件队列。

六、用等待预算理解为什么需要 tryLock

假设请求总超时 300 ms,获取锁最多只能占 50 ms,剩余 250 ms 留给数据库与响应。tryLock(50, MILLISECONDS) 失败后可以快速降级,而 synchronized 无法直接表达这个等锁预算。

if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
    try {
        updateState();
    } finally {
        lock.unlock();
    }
} else {
    rejectOrDegrade();
}

超时不等于逻辑自动安全:失败分支要保证没有写入一半,成功分支必须在 finally 释放。若临界区内又调用慢远程服务,即使锁获取有超时,持锁时间仍可能失控。

记忆钩子:默认先选结构简单、自动释放的 synchronized;只有明确需要“可中断、可超时、公平策略、多 Condition”时,ReentrantLock 的额外复杂度才有价值。

七、常见误区与追问

  • 误区:ReentrantLock 因为是 JDK 类,所以一定比 synchronized 快。 性能取决于 JDK、竞争和临界区,选型应先看语义能力与实测。
  • 误区:调用 lock() 后可以在任意位置随手 unlock() 获取成功后必须用紧邻的 try-finally 配对,否则异常会永久占锁。
  • 误区:公平锁保证每个线程严格按到达时间执行。 它倾向按队列顺序授予锁,但调度和特定 API 路径不构成硬实时承诺。
  • 追问:为什么 synchronized 更不容易泄漏锁? JVM 在正常和异常退出同步区域时自动释放监视器,不依赖业务代码手工调用。
  • 追问:lockInterruptibly() 解决什么问题? 线程在等锁期间可响应中断,便于取消任务或打破长等待链。
  • 追问:多个 Condition 有什么价值? 可把“非空”“未满”等不同条件的等待者分组,减少无关唤醒。
  • 追问:二者都可重入吗? 是,同一线程可重复进入,但必须匹配相同次数的退出或 unlock。

八、加强记忆

两者都提供互斥、可见性与可重入,区别在“自动语法”与“显式控制”:synchronized 结构化、异常自动释放,适合绝大多数简单临界区;ReentrantLock 基于 AQS,需要 finally 解锁,却换来可中断等待、tryLock 超时、公平选项和多个 Condition。不要用“谁绝对更快”选型,而要先问业务是否有锁等待预算、取消需求和多条件协调。无论选哪种,缩短临界区、避免锁内 I/O 和保持锁对象/锁实例稳定都比换 API 更重要。