ReentrantLock 与 synchronized 底层实现对比图解

一、整体架构与核心载体总览图

复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                      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自动释放锁,唤醒等待线程,根据竞争情况在安全点降级锁

关键底层补充

  1. Mark Word 是核心:64 位 JVM 中,不同锁状态下,Mark Word 存储的内容完全不同,偏向锁存线程 ID、Epoch;轻量级锁存 Lock Record 指针;重量级锁存 monitor 指针。
  2. Monitor 管程是 JVM C++ 实现的核心,包含_owner(持有锁的线程)、_cxq(竞争队列)、_WaitSet(wait 等待队列)、_recursions(重入次数)。
  3. 锁升级是单向不可逆的(除偏向锁可批量重偏向 / 撤销),只能从低到高升级,仅在全局安全点才会降级。

三、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() 唤醒队列中头节点的后继线程
        ↓
⑥ 被唤醒的线程回到步骤③,再次尝试抢锁

关键底层补充

  1. 核心是内部的 Sync 类,继承自 AQS,分为NonfairSync(默认非公平)和FairSync(公平)两个实现,唯一区别是tryAcquire时是否检查 CLH 队列是否有等待线程。
  2. AQS 三大核心要素:
    • volatile int state:0 = 无锁,>0 = 已被持有,数值代表重入次数
    • CLH 双向队列:Node 节点封装线程、等待状态、前驱后继指针,实现线程的 FIFO 排队阻塞
    • ConditionObject:实现 Condition 接口,每个 Condition 对应一个单向等待队列,实现 await/signal,替代 wait/notify,支持多条件精准唤醒。
  3. 线程阻塞 / 唤醒使用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 的可控性更有优势。
  • synchronizedJVM 虚拟机层面 的原生锁,由 C++ 实现的 monitor 管程机制兜底,javac 编译时会自动在同步代码块前后插入monitorentermonitorexit字节码指令。它的核心载体是 Java 对象的对象头Mark Word,JDK6 及之后引入了自适应的锁升级链路:无锁→偏向锁→轻量级锁→重量级锁,只有在高竞争场景下才会膨胀到依赖操作系统内核态 mutex 互斥量的重量级锁,JVM 会自动对它做优化。
  • ReentrantLockJDK 应用层 的锁实现,属于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()判断锁是否被持有,方便线上问题排查和监控。
相关推荐
jaycyj2 小时前
pytest
开发语言·python
A_aspectJ2 小时前
【Java基础开发】基于 Java Swing +MySQL + JDBC 版实现图书管理系统
java·开发语言·mysql
Gary Studio2 小时前
安卓HAL编译流程
开发语言·python
我是无敌小恐龙2 小时前
Java SE 零基础入门Day06 方法重载+Debug调试+String字符串全套API详解(超全干货)
java·开发语言·人工智能·python·transformer·无人机·量子计算
AI+程序员在路上2 小时前
Qt6读取rtsp视频流的几种方法
开发语言·qt6.3
码农飞哥2 小时前
从Java后端到AI应用开发,我这两年做了什么
java·开发语言·人工智能
胡童嘉2 小时前
C语言考研《谭浩强C语言》教材第一章理论+实践汇总
c语言·开发语言·考研
初心未改HD3 小时前
Go语言Slice切片底层原理深度解析
开发语言·golang
程序员三明治3 小时前
【AI】Java 调用大模型 API 实战:从 OpenAI 协议到 SiliconFlow 流式响应解析
java·开发语言·人工智能