一、整体架构与核心载体总览图
┌─────────────────────────────────────────────────────────────────────────────┐
│ ReentrantLock vs synchronized 底层总览 │
├───────────────────────────────┬─────────────────────────────────────────────┤
│ 核心维度 │ 实现载体与归属 │
├───────────────────────────────┼─────────────────────────────────────────────┤
│ 代码归属层级 │ 【ReentrantLock】JDK应用层 → java.util.concurrent.locks包 │
│ │ 【synchronized】JVM虚拟机层 → C++实现的monitor管程机制 │
├───────────────────────────────┼─────────────────────────────────────────────┤
│ 核心同步底座 │ 【ReentrantLock】AQS抽象队列同步器 │
│ │ ↳ volatile int state(锁状态+重入次数) │
│ │ ↳ CLH双向同步队列(排队等待线程) │
│ │ ↳ 多个Condition单向条件队列(精准等待唤醒)│
│ │ │
│ │ 【synchronized】Java对象头 + Monitor管程 │
│ │ ↳ Mark Word(锁标记、线程ID、分代年龄等) │
│ │ ↳ 自适应锁升级链路:无锁→偏向锁→轻量级锁→重量级锁│
│ │ ↳ 操作系统内核态mutex互斥量 │
├───────────────────────────────┼─────────────────────────────────────────────┤
│ 锁生命周期管理 │ 【ReentrantLock】手动lock()/unlock(),必须在finally中释放 │
│ │ 【synchronized】JVM自动管理,退出同步块/发生异常自动释放 │
└───────────────────────────────┴─────────────────────────────────────────────┘
二、synchronized 底层实现与锁升级全流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ synchronized 底层执行流程 + 锁升级全链路(JDK6+) │
└─────────────────────────────────────────────────────────────────────────────┘
① 线程进入synchronized修饰的方法/代码块,锁定对应对象
↓
② 读取锁对象的【对象头Mark Word】(64位虚拟机核心存储单元)
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ 自适应锁升级判断链路 │
├─────────────────────────────────────────────────────────────────────────────┤
│ 无锁状态(标记位001):对象无锁定,CAS设置偏向锁线程ID → 成功→进入偏向锁状态 │
│ ↓ 失败(已有线程占用) │
│ 偏向锁状态(标记位101):校验Mark Word中的线程ID是否为当前线程 → 是→直接重入 │
│ ↓ 否(出现线程竞争),撤销偏向锁,升级为轻量级锁 │
│ 轻量级锁状态(标记位00):线程栈帧创建Lock Record,CAS复制Mark Word到锁记录 │
│ ↓ CAS成功→加锁成功;失败则开启自适应自旋重试 │
│ ↓ 自旋达到阈值仍失败,存在严重竞争,升级为重量级锁 │
│ 重量级锁状态(标记位10):Mark Word存储monitor对象指针,线程进入内核态阻塞队列 │
│ ↓ 依赖操作系统mutex互斥量,线程阻塞/唤醒需用户态↔内核态切换,开销极大 │
└─────────────────────────────────────────────────────────────────────────────┘
↓
③ 加锁成功,执行同步业务代码
↓
④ 退出同步块/发生异常,JVM自动释放锁,唤醒等待线程,根据竞争情况在安全点降级锁
关键底层补充
- Mark Word 是核心:64 位 JVM 中,不同锁状态下,Mark Word 存储的内容完全不同,偏向锁存线程 ID、Epoch;轻量级锁存 Lock Record 指针;重量级锁存 monitor 指针。
- Monitor 管程是 JVM C++ 实现的核心,包含
_owner(持有锁的线程)、_cxq(竞争队列)、_WaitSet(wait 等待队列)、_recursions(重入次数)。 - 锁升级是单向不可逆的(除偏向锁可批量重偏向 / 撤销),只能从低到高升级,仅在全局安全点才会降级。
三、ReentrantLock 底层实现流程图(基于 AQS)
┌─────────────────────────────────────────────────────────────────────────────┐
│ ReentrantLock 底层执行流程图(默认非公平锁) │
└─────────────────────────────────────────────────────────────────────────────┘
① 线程调用 lock() 方法 → 委托给内部Sync类(AQS的子类)
↓
② 【非公平锁快速抢锁】CAS修改volatile state变量(0→1)
├─ 成功:设置当前线程为锁的独占持有者 → 加锁成功,执行业务代码
└─ 失败:调用acquire(1),进入AQS核心排队逻辑
↓
③ AQS核心acquire全流程
├─ 第一步:tryAcquire() 再次尝试获取锁
│ ├─ state=0:非公平锁直接CAS抢锁;公平锁先检查队列是否有等待线程,无才抢锁
│ ├─ state>0且持有线程是当前线程:state+1(可重入计数)→ 获取成功
│ └─ 获取失败 → 进入下一步
├─ 第二步:addWaiter() 将当前线程封装为Node节点,CAS加入【CLH双向同步队列】尾部
├─ 第三步:acquireQueued() 线程在队列中自旋+阻塞
│ ├─ 若前驱节点是头节点,再次尝试抢锁,成功则出队,成为新的头节点
│ └─ 抢锁失败,调用LockSupport.park() 阻塞线程,等待前驱节点unpark唤醒
└─ 线程被中断则抛出异常,终止流程
↓
④ 业务执行完毕,调用 unlock() → 委托给AQS的release(1)
↓
⑤ 释放锁全流程
├─ tryRelease():state-1,若state=0,清空持有线程,锁完全释放
└─ 释放成功后,调用LockSupport.unpark() 唤醒队列中头节点的后继线程
↓
⑥ 被唤醒的线程回到步骤③,再次尝试抢锁
关键底层补充
- 核心是内部的 Sync 类,继承自 AQS,分为
NonfairSync(默认非公平)和FairSync(公平)两个实现,唯一区别是tryAcquire时是否检查 CLH 队列是否有等待线程。 - AQS 三大核心要素:
volatile int state:0 = 无锁,>0 = 已被持有,数值代表重入次数- CLH 双向队列:Node 节点封装线程、等待状态、前驱后继指针,实现线程的 FIFO 排队阻塞
ConditionObject:实现 Condition 接口,每个 Condition 对应一个单向等待队列,实现 await/signal,替代 wait/notify,支持多条件精准唤醒。
- 线程阻塞 / 唤醒使用
LockSupport.park()/unpark(),无需在同步块中调用,不会抛出IllegalMonitorStateException,比 wait/notify 更灵活。
四、核心特性底层实现差异对比图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 核心特性底层实现差异图解 │
├───────────────┬───────────────────────────┬───────────────────────────────────┤
│ 特性 │ ReentrantLock │ synchronized │
├───────────────┼───────────────────────────┼───────────────────────────────────┤
│ 公平锁实现 │ AQS队列严格FIFO,抢锁前 │ 无公平实现,线程唤醒后依然CAS抢锁 │
│ │ 先检查队列是否有等待线程 │ 无法保证先来先执行 │
├───────────────┼───────────────────────────┼───────────────────────────────────┤
│ 可中断等待 │ lockInterruptibly()实现, │ 无法响应中断,阻塞后必须等到获取锁 │
│ │ park时可响应中断抛出异常 │ 才能释放CPU │
├───────────────┼───────────────────────────┼───────────────────────────────────┤
│ 条件队列 │ 多个Condition单向队列, │ 仅1个waitSet等待队列,notifyAll() │
│ │ signal()可精准唤醒指定队列 │ 会唤醒所有等待线程,产生惊群效应 │
├───────────────┼───────────────────────────┼───────────────────────────────────┤
│ 性能核心 | 全程用户态,park/unpark仅 | 轻量级锁用户态自旋,重量级锁需内核态 |
│ | 必要时进入内核态,无锁升级 | 切换,高竞争下性能损耗大 │
└───────────────┴───────────────────────────┴───────────────────────────────────┘
最终使用建议
常规场景优先用 synchronized,只有需要它不支持的高级特性时,才选择 ReentrantLock。
版本二:3 分钟深度完整版(二面 / 深挖环节,展现实力,拉开差距)
面试官您好,synchronized 和 ReentrantLock 是 Java 并发编程中最核心的两个独占可重入锁,二者的核心目标都是解决多线程环境下共享资源的线程安全问题,但从底层实现到上层使用、再到场景适配,都有本质的区别,我系统地给您展开说明:
第一,底层实现与归属层级完全不同,这是所有差异的根源
第二,使用方式与生命周期管理差异显著
第三,核心功能特性上,ReentrantLock 对 synchronized 做了全面的能力扩展
这也是二者最核心的使用差异,synchronized 仅能满足基础的同步需求,而 ReentrantLock 提供了大量高级特性,解决了 synchronized 的很多使用痛点:
第四,性能表现上,二者在不同场景下各有优势
很多人有一个误区,认为 ReentrantLock 性能一定比 synchronized 好,这个结论只适用于 JDK6 之前的版本。JDK6 及之后,JVM 对 synchronized 做了大量深度优化,包括偏向锁、轻量级锁、自适应自旋、锁消除、锁粗化等,在低竞争、常规的并发场景下,synchronized 的性能和 ReentrantLock 基本持平,甚至因为 JVM 的自动优化,表现更优。只有在高竞争、复杂的并发场景下,ReentrantLock 的精细化可控性,才能体现出稳定的性能优势。
最后是我的选型建议
在日常开发中,常规的同步场景优先使用 synchronized ,它使用简单、无手动释放锁的风险,JVM 会自动做优化,也是 Java 官方推荐的首选同步方案。只有当业务场景需要 synchronized 不支持的高级特性时,才选择 ReentrantLock,比如需要公平锁、可中断等待、超时获取锁、多条件精准唤醒的复杂并发场景,比如自定义同步组件、生产者消费者模型、限流熔断等场景。
另外补充一点,二者都是可重入锁,同一个线程多次获取同一把锁不会死锁,底层都是通过计数器实现重入计数,synchronized 是在 monitor 的_recursions字段记录,ReentrantLock 是在 AQS 的state变量中记录,这是二者为数不多的共性。
- 常规同步场景、追求代码简洁性:优先用
synchronized,JVM 自动优化,无死锁风险。 - 复杂并发场景(需要公平锁、可中断、超时获取、多条件精准唤醒):使用
ReentrantLock,必须在 finally 块中执行 unlock (),避免锁泄漏。
面试场景完美回答(分两个版本,适配不同面试环节)
版本一:1 分钟精简版(初筛 / 快问快答环节,抓核心不啰嗦)
synchronized 和 ReentrantLock 都是 Java 中实现线程安全的可重入独占锁,核心解决多线程并发下共享资源的原子性问题,二者核心差异我从 4 个核心维度说明:
- 底层归属不同 :synchronized 是JVM 虚拟机层面 实现,靠对象头 Mark Word 锁标记、monitor 管程机制,自带锁升级优化;ReentrantLock 是JDK 应用层实现,基于 AQS 抽象队列同步器,用 volatile state 和 CLH 双向队列实现同步。
- 使用方式不同:synchronized 是隐式锁,自动获取 / 释放锁,异常时 JVM 自动释放,无死锁风险;ReentrantLock 是显式锁,必须手动 lock () 加锁、finally 中 unlock () 释放,编码要求更高。
- 功能灵活性不同:synchronized 仅支持阻塞式非公平锁;ReentrantLock 支持公平锁、可中断等待、超时获取锁、多 Condition 条件队列精准唤醒,还有锁状态监控 API,功能更丰富。
- 性能表现不同:JDK6 之后,JVM 对 synchronized 做了偏向锁、轻量级锁等优化,常规场景下二者性能基本持平;仅高竞争、复杂并发场景下,ReentrantLock 的可控性更有优势。
- synchronized 是JVM 虚拟机层面 的原生锁,由 C++ 实现的 monitor 管程机制兜底,javac 编译时会自动在同步代码块前后插入
monitorenter和monitorexit字节码指令。它的核心载体是 Java 对象的对象头Mark Word,JDK6 及之后引入了自适应的锁升级链路:无锁→偏向锁→轻量级锁→重量级锁,只有在高竞争场景下才会膨胀到依赖操作系统内核态 mutex 互斥量的重量级锁,JVM 会自动对它做优化。 - ReentrantLock 是JDK 应用层 的锁实现,属于
java.util.concurrent.locks包,完全由 Java 代码实现,核心底座是 AQS(抽象队列同步器)。它通过volatile修饰的state变量标记锁状态(0 = 无锁,>0 代表锁被持有,数值对应重入次数),配合 CLH 双向同步队列实现线程的 FIFO 排队,通过LockSupport.park()/unpark()实现线程的阻塞与唤醒,全程逻辑对开发者透明可控。 - synchronized 是隐式锁,无需开发者手动干预锁的生命周期。进入同步方法 / 代码块时自动加锁,代码正常执行完成、或者发生未捕获异常时,JVM 会自动释放锁,不会因为代码异常导致锁泄漏和死锁,使用门槛极低,代码简洁不易出错。
- ReentrantLock 是显式锁,锁的获取和释放完全由开发者手动控制。必须手动调用
lock()方法加锁,且必须在 finally 代码块中调用 unlock () 释放锁,否则一旦业务代码抛出异常,锁会永远无法释放,导致锁泄漏和永久死锁,使用灵活性更高,但对编码规范的要求也更高。 - 公平性支持 :synchronized 只支持非公平锁,线程被唤醒后依然是 CAS 随机抢锁,无法保证先来先服务;ReentrantLock 默认是非公平锁,但可以通过构造函数传入
true开启公平锁,严格遵循 AQS 队列的 FIFO 规则,等待时间最长的线程优先获取锁,避免线程饥饿。 - 锁获取的灵活性 :synchronized 只有阻塞式获取锁一种方式,线程一旦开始等待,就无法中断、没有超时机制,只能一直阻塞直到获取到锁,很容易在死锁场景中无限阻塞;ReentrantLock 提供了丰富的锁获取方式:
tryLock()支持非阻塞抢锁,成功返回 true、失败立即返回不阻塞;tryLock(long time, TimeUnit unit)支持超时等待,超时未获取到锁就主动放弃;lockInterruptibly()支持可中断等待,线程在等待锁的过程中可以响应中断信号,终止等待,完美规避死锁场景。 - 条件等待机制 :synchronized 只能通过对象的
wait()/notify()/notifyAll()实现等待唤醒,只能关联一个等待队列,notifyAll()会唤醒所有等待的线程,极易产生惊群效应,造成无效的 CPU 开销;ReentrantLock 可以通过newCondition()创建多个独立的Condition条件对象,每个 Condition 对应一个独立的单向等待队列,可以实现精准唤醒,比如生产者和消费者分别绑定两个 Condition,只唤醒对应队列的线程,完全避免惊群效应,大幅提升并发效率。 - 锁状态可监控 :synchronized 无法在应用代码中查询锁的状态,比如锁是否被持有、被哪个线程持有、重入次数是多少,问题排查难度大;ReentrantLock 提供了丰富的监控 API,比如
isHeldByCurrentThread()判断当前线程是否持有锁、getHoldCount()获取当前线程的重入次数、isLocked()判断锁是否被持有,方便线上问题排查和监控。