synchronized 和 ReentrantLock 的区别是什么?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常希望考察以下几个由浅入深的层次:

  1. 基础功能认知:你是否了解两者都是 Java 中实现线程同步、保证并发安全的机制,并且都是可重入锁
  2. 差异化对比能力:这是核心。面试官不仅想听你罗列区别,更想知道你是否能从 API 使用、功能性、性能特性 等多个维度进行结构化对比。
  3. 底层原理理解:你是否清楚 synchronizedJVM 内置的、基于底层管程(Monitor)实现的隐式锁,而 ReentrantLockJDK 层面通过抽象队列同步器(AQS)实现的显式锁
  4. 场景化选型能力:能否根据具体场景(如是否需要公平性、可中断、超时等待、多个条件变量)做出合理的技术选型,并说明理由。这能反映你的实战经验和架构思维。
  5. 版本演进认知:是否了解随着 JDK 版本(尤其是 JDK 6 引入的锁优化)的迭代,二者性能差距的变化,避免给出过时的结论。

核心答案

synchronized 是 Java 关键字,属于 JVM 层面的内置隐式锁ReentrantLockjava.util.concurrent.locks 包下的类,属于 API 层面的显式锁。它们的主要区别体现在使用方式、功能性、灵活性和底层实现上。

一句话概括synchronized 简洁、自动ReentrantLock 强大、灵活。在无需高级功能的场景下,优先推荐使用 synchronized;当需要可中断锁、超时获取、公平锁、多个条件变量等高级功能时,则必须使用 ReentrantLock

深度解析

使用方式与灵活性

  • synchronized: 无需手动释放锁。它的作用域是 代码块(需指定锁对象)或 实例方法/静态方法(锁对象分别是当前实例和 Class 对象)。锁的获取和释放由编译器生成的字节码指令(monitorenter/monitorexit)隐式管理。
// synchronized 代码块
public void syncMethod() {
    synchronized (this) { // 手动指定锁对象
        // 临界区
    }
}
// synchronized 方法
public synchronized void syncMethod() { // 锁是当前实例 this
    // 临界区
}
  • ReentrantLock: 必须 显式地调用 lock()unlock() 方法,并且通常将 unlock() 置于 finally 块中以确保锁必然释放,防止死锁。这赋予了它更大的灵活性,但也增加了代码的复杂性。
ReentrantLock lock = new ReentrantLock();
public void reentrantLockMethod() {
    lock.lock(); // 必须显式获取
    try {
        // 临界区
    } finally {
        lock.unlock(); // 必须显式释放,且在finally中
    }
}

功能性对比

这是 ReentrantLock 优势最集中的地方。

特性synchronizedReentrantLock
公平性非公平锁(无法指定)可指定为公平或非公平锁(构造方法传入 true)。公平锁能减少线程饥饿,但吞吐量通常较低。
尝试非阻塞获取不支持支持 tryLock(),可立即返回是否成功。
可中断等待等待锁时不可中断,会一直阻塞。支持 lockInterruptibly(),等待锁的线程可被其他线程中断。
超时获取不支持支持 tryLock(long timeout, TimeUnit unit),可避免无限期等待。
条件变量仅有一个隐式的等待/通知机制(Object.wait()/notify())。可以创建多个 Condition 对象,实现更精细的线程间通信(如“生产者-消费者”模型)。

底层实现与性能

  • synchronized: 依赖 JVM 的 ObjectMonitor(对象监视器)实现。在 JDK 6 之后,JVM 团队对其进行了大量优化,如偏向锁、轻量级锁、锁消除、锁粗化、适应性自旋等。在低至中度竞争的场景下,其性能已经非常优秀,甚至与 ReentrantLock 相差无几。
  • ReentrantLock: 基于 AQS(AbstractQueuedSynchronizer) 实现。AQS 内部维护了一个 CLH 变体的双向队列来管理等待线程。其高级功能(如条件变量)正是基于 AQS 的灵活架构实现的。

最佳实践与常见误区

  1. 优先使用 synchronized: 由于其简洁性和 JVM 的持续优化,在大多数通用并发场景下,synchronized 是首选。它能减少代码错误(如忘记解锁),且性能足够好。
  2. 需要高级功能时再用 ReentrantLock: 当你的业务确实需要上述表格中的某个高级特性时,再选择 ReentrantLock切忌为了用而用
  3. 必须解锁在 finally: 使用 ReentrantLock 时,这是铁律,否则一旦临界区代码抛出异常,锁将永远无法释放。
  4. 避免误解性能: 不要再说“ReentrantLock 性能远高于 synchronized”,这个结论在 JDK 6 之后已经过时。性能差异高度依赖于具体场景(竞争激烈程度、锁持有时间等)。在极高竞争下,ReentrantLock 的可预测性可能更好。

总结

synchronized 是 JVM 提供的 自动化、易用的基础锁,而 ReentrantLock 是 JDK 提供的 功能丰富、可控性高的高级锁。选择的关键在于 业务需求:追求简单可靠选 synchronized;需要超时、中断、公平性或复杂等待通知机制时,则选 ReentrantLock