根爱ai创作助手:Java并发面试必问——synchronized vs ReentrantLock全解析
北京时间为2026年4月10日,正值2026春招季与年中面试高峰,Java并发编程已成为大厂技术面试中的核心高频考点-8。本文由根爱ai创作助手精心打磨,从基础概念到底层原理,从代码示例到面试要点,带你彻底吃透这对面试官最爱问的“锁兄弟”。
📌 本文你将掌握

synchronized 与 Lock(ReentrantLock)的本质区别
synchronized 锁升级机制(无锁→偏向锁→轻量级锁→重量级锁)

ReentrantLock 底层 AQS 实现原理
生产-消费者场景代码实战
高频面试题标准答案
在多线程编程中,共享资源的线程安全是一个无法回避的核心问题。synchronized 作为 Java 语言层面的内置锁,凭借“简单易用”成为无数初学者的第一选择;而 ReentrantLock 作为 JUC 包中功能更强大的显式锁,则是中高级开发者在复杂场景下的不二之选。
很多开发者对这个知识点的掌握停留在表面:知道两者都是锁,却说不出本质区别;会用 synchronized,却不理解锁升级机制;记得 ReentrantLock 比 synchronized 灵活,却说不出 AQS 的实现原理。这些恰恰是面试官最想考察的地方。
本文将由浅入深,从痛点切入到原理剖析,从代码实战到面试要点,帮你建立完整的知识链路。
一、痛点切入:为什么需要显式锁?
先看一个经典的计数器场景:
// 使用 synchronized 的实现(旧方式) public class Counter { private int count = 0; public synchronized void increment() { count++; // 实际上 count++ 不是原子操作:读取→加1→写回 } public int getCount() { return count; } }
在单线程环境下没有问题,但多线程同时调用 increment() 时,count++ 的三条指令交错执行,就会出现数据竞争(Race Condition),最终结果小于预期-67。synchronized 通过互斥锁解决了数据竞争问题,但它的局限性也很明显:
无法中断:线程在等待锁时只能被阻塞,无法响应中断信号
无超时机制:可能永久等待,无法设置获取锁的超时时间
仅支持非公平锁:无法保证等待时间最长的线程先获得锁
单一等待队列:通过
wait()/notify()只能实现一个等待队列
正是为了弥补这些不足,Java 5 引入了 java.util.concurrent.locks 包,其中 ReentrantLock 提供了更强大的锁控制能力-61。
二、核心概念讲解:synchronized(内置锁)
定义
synchronized 是 Java 提供的一个关键字,JVM 层面的隐式锁(Intrinsic Lock),无需手动释放,JVM 会自动完成加锁和解锁操作。
核心特点
可重入性:同一线程可以多次获取同一把锁
默认非公平锁:线程竞争时随机获取锁,没有公平性保证
自动加锁/解锁:方法或代码块执行完自动释放,无需手动处理
底层依赖:对象头的 Mark Word + 监视器锁(ObjectMonitor)
三种使用方式
public class SynchronizedDemo { // 1. 实例方法锁:锁当前对象 this public synchronized void objectLock() { System.out.println(Thread.currentThread().getName() + " 获取对象锁"); } // 2. 静态方法锁:锁 Class 对象 public static synchronized void classLock() { System.out.println(Thread.currentThread().getName() + " 获取类锁"); } // 3. 代码块锁:自定义锁对象(灵活度最高) private final Object lockObj = new Object(); public void blockLock() { synchronized (lockObj) { System.out.println(Thread.currentThread().getName() + " 获取代码块锁"); } } }
-22
锁升级机制(JDK 1.6 后的关键优化)
早期版本中,synchronized 被称为“重量级锁”,每次加锁都需要操作系统介入,性能较差。JDK 1.6 引入了锁升级机制,锁状态会经历以下路径-21:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁| 锁状态 | 特点 | 适用场景 |
|---|---|---|
| 偏向锁 | 偏向第一个线程,只做标记,不加真锁 | 单线程反复访问同一资源 |
| 轻量级锁 | 使用 CAS(Compare-And-Swap)尝试获取锁,自旋等待 | 多线程短时间执行,竞争不激烈 |
| 重量级锁 | 阻塞等待,线程进入等待队列,不再消耗 CPU | 高竞争、锁持有时间长的场景 |
-67
理解偏向锁:偏向锁不是真的加锁,而是在对象头中做一个标记,记录这个锁属于哪个线程。如果后续没有其他线程竞争,就可以避免加锁解锁的开销。只有当竞争出现时,才升级到轻量级锁-21。
锁升级是不可逆的:一旦升级到重量级锁,无法回退到轻量级锁,需要新的锁对象才能重新开始-21。
三、关联概念讲解:ReentrantLock(显式锁)
定义
ReentrantLock 是 java.util.concurrent.locks.Lock 接口的实现类,位于 JUC 包中,由并发编程大师 Doug Lea 编写,是一种需要手动加锁和释放锁的显式锁-22。
核心特点
可重入性:支持同一线程重复获取锁
可选择公平/非公平:通过构造函数指定
支持中断:
lockInterruptibly()支持响应中断支持超时:
tryLock(timeout, unit)设置等待时限支持多条件队列:通过
Condition实现精细的线程协作
基本使用方式
public class ReentrantLockDemo { // 公平锁(按请求顺序获取),也可传 false 或使用无参构造(默认非公平) private static final ReentrantLock lock = new ReentrantLock(true); public static void doTask() { lock.lock(); // 手动加锁 try { // 临界区代码 System.out.println(Thread.currentThread().getName() + " 获取锁"); } catch (Exception e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); // 必须手动释放,否则死锁! } } }
-22
公平锁 vs 非公平锁
ReentrantLock 通过构造函数区分公平锁和非公平锁-37:
// 非公平锁(默认,性能更高) ReentrantLock lock = new ReentrantLock(); // 公平锁 ReentrantLock fairLock = new ReentrantLock(true);
公平锁:按等待时间顺序获取锁,避免线程饥饿,但性能较低,因为需要维护队列顺序-37。
非公平锁:允许新来的线程直接竞争锁,能插队成功就立即占有,失败再进入队列排队。这种“插队”策略减少了上下文切换,性能更高-37。
为什么默认是非公平锁? 公平锁虽然公平,但每个线程都要加入队列排队、阻塞、再由阻塞变为运行,上下文切换开销很大。非公平锁允许插队,减少了线程切换,在大多数场景下性能更优-。
四、概念关系与区别总结
synchronized 是 JVM 层面的语言级锁,追求简洁易用;ReentrantLock 是基于 AQS 框架的API 级锁,追求灵活可扩展。
一句话记忆:synchronized 是 JVM 帮你自动管理的“傻瓜锁”,ReentrantLock 是你自己掌管的“瑞士军刀锁”。
详细对比表
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层级 | JVM 内置关键字,通过监视器实现 | JDK API 类,基于 AQS 实现 |
| 使用方式 | 隐式获取/释放,自动完成 | 显式调用 lock()/unlock() |
| 可重入性 | ✅ 支持 | ✅ 支持 |
| 锁类型 | 仅支持非公平锁 | 支持公平锁和非公平锁 |
| 条件变量 | wait()/notify(),单一队列 | 可创建多个 Condition 对象 |
| 中断响应 | ❌ 不支持 | ✅ lockInterruptibly() 支持 |
| 超时机制 | ❌ 不支持 | ✅ tryLock(timeout, unit) 支持 |
| 性能对比 | JDK 1.6 优化后性能接近 | 高竞争场景表现更稳定 |
-11
五、代码示例:生产者-消费者模型
一个实际的生产者-消费者场景最能体现两者的差异。以下是使用 ReentrantLock + Condition 的实现:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ProducerConsumerDemo { // 可消费的产品数量 private static int productNum = 0; private static final ReentrantLock lock = new ReentrantLock(); // 为生产者、消费者分别创建不同的等待队列 private static final Condition consumer = lock.newCondition(); private static final Condition producer = lock.newCondition(); public static void main(String[] args) { // 生产者线程 new Thread(() -> { while (true) { lock.lock(); try { // 有产品可消费时,生产者等待 while (productNum > 0) { producer.await(); } // 生产 1-4 件产品 int total = (int) (Math.random() 4) + 1; for (int i = 0; i < total; i++) { System.out.println("生产者生产了产品,当前数量:" + (++productNum)); } // 唤醒消费者 consumer.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }, "生产者").start(); // 消费者线程 new Thread(() -> { while (true) { lock.lock(); try { // 无产品可消费时,消费者等待 while (productNum == 0) { consumer.await(); } // 消费所有产品 int total = productNum; while (productNum > 0) { System.out.println("消费者消费了产品,剩余数量:" + (--productNum)); } // 唤醒生产者 producer.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }, "消费者").start(); } }
-47
如果使用 synchronized 实现上述逻辑,只能通过一个 wait/notify 队列实现,无法区分生产者和消费者的等待原因,容易造成“生产者唤醒生产者”或“消费者唤醒消费者”的低效情况。
六、底层原理:技术支撑
synchronized 的底层支撑
对象头(Mark Word) :锁信息记录在 Java 对象头的 Mark Word 中
监视器锁(ObjectMonitor) :重量级锁阶段,每个对象关联一个 Monitor 对象
字节码指令:编译后在同步代码块前后插入
monitorenter和monitorexit指令CAS 操作:轻量级锁阶段通过 CAS 实现自旋获取锁
ReentrantLock 的底层支撑:AQS
AQS(AbstractQueuedSynchronizer) 是 JUC 包中构建锁和同步器的核心框架,ReentrantLock、CountDownLatch、Semaphore 都构建在它之上-35。
AQS 的核心架构包含三大组件-35:
volatile int state:同步状态,
state=0表示无锁,state>0表示有锁(值表示重入次数)CLH 虚拟双向队列:FIFO 等待队列,用于存放获取锁失败的线程
Node 节点:封装等待线程及其状态(SIGNAL、CANCELLED 等)
当线程获取锁失败时,会被封装成 Node 节点加入队列尾部并阻塞;持有锁的线程释放锁时,会唤醒队列中的后继节点-35-。
七、高频面试题与参考答案
Q1:synchronized 和 ReentrantLock 有什么区别?
参考答案(分点作答,逻辑清晰):
实现层级不同:synchronized 是 JVM 关键字,底层由
monitorenter/monitorexit指令实现;ReentrantLock 是 JDK API 类,基于 AQS 框架实现。使用方式不同:synchronized 隐式加锁/释放,无需手动操作;ReentrantLock 需显式调用
lock()/unlock(),且unlock()必须放在finally中。锁类型不同:synchronized 仅支持非公平锁;ReentrantLock 支持公平锁和非公平锁。
中断能力不同:synchronized 等待锁时不可中断;ReentrantLock 支持
lockInterruptibly()。超时机制不同:synchronized 无超时机制;ReentrantLock 支持
tryLock(timeout, unit)。条件队列不同:synchronized 只有一个等待队列(
wait/notify);ReentrantLock 可创建多个Condition对象,实现精细化的线程协作。
Q2:synchronized 的锁升级机制是什么?
参考答案:
JDK 1.6 对 synchronized 进行了优化,引入锁升级机制,锁状态变化路径为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,且不可逆。
偏向锁:偏向第一个线程,只做标记不加真锁,避免加锁开销。无竞争时保持此状态。
轻量级锁:出现锁竞争时升级,通过 CAS 自旋尝试获取锁,不阻塞线程,避免操作系统调度开销。
重量级锁:竞争激烈或自旋次数超限时升级,线程阻塞等待,通过 ObjectMonitor 实现。
锁升级使 synchronized 在低竞争场景性能接近 ReentrantLock。
Q3:ReentrantLock 的公平锁和非公平锁有什么区别?为什么默认是非公平锁?
参考答案:
公平锁:按线程等待时间顺序获取锁(FIFO),避免线程饥饿,但需要维护队列,上下文切换频繁,性能较低。
非公平锁:新来的线程先尝试插队竞争锁,成功则直接获取,失败再进入队列排队。性能更高,但可能导致线程饥饿。
为什么默认非公平锁:公平锁需要每次都要阻塞和唤醒线程,上下文切换开销较大;非公平锁允许插队,减少了线程切换,在大多数场景下吞吐量更高。
Q4:什么是 AQS?ReentrantLock 如何利用 AQS 实现?
参考答案:
AQS(AbstractQueuedSynchronizer)是 JUC 包中构建锁和同步器的核心框架。核心由三部分组成:volatile int state(同步状态,0 表示无锁,>0 表示有锁及重入次数)、CLH 虚拟双向 FIFO 队列(存放等待锁的线程)、Node 节点(封装线程及其等待状态)。
ReentrantLock 通过内部 Sync 类继承 AQS,并实现 tryAcquire()/tryRelease() 方法。公平锁与非公平锁的区别体现在 tryAcquire() 的实现上——公平锁先检查队列中是否有前驱节点,有则排队;非公平锁先尝试 CAS 插队。
Q5:如何选择使用 synchronized 还是 ReentrantLock?
参考答案:
简单同步需求(如方法/代码块互斥),优先选 synchronized,代码简洁且不易出错。
需要公平锁、可中断、超时获取、多条件队列等高级特性时,选 ReentrantLock。
高竞争、复杂同步场景选 ReentrantLock,低竞争或简单场景选 synchronized。
性能参考:JDK 1.6 优化后,低竞争场景 synchronized 略优,高竞争场景 ReentrantLock 更稳定-61。
八、总结与进阶预告
核心知识点回顾
| 知识点 | 要点 |
|---|---|
| synchronized 本质 | JVM 内置关键字,隐式锁,支持锁升级 |
| ReentrantLock 本质 | JUC API 显式锁,基于 AQS 框架 |
| 核心区别 | 自动/手动、公平性、中断、超时、条件队列 |
| 底层原理 | synchronized → 对象头 + ObjectMonitor;ReentrantLock → AQS + CAS + CLH 队列 |
易错点提醒
ReentrantLock必须在finally中调用unlock(),否则会造成死锁公平锁并非性能最优选择,需要根据实际场景权衡
synchronized的锁升级不可逆,高竞争下会升级为重量级锁lockInterruptibly()必须在try-catch中捕获InterruptedException
进阶预告
下一篇将深入剖析 AQS 源码实现,从 acquire() 到 release(),从独占模式到共享模式,彻底搞懂 JUC 并发工具的核心底层原理,敬请期待!