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/
面试考察点
-
锁机制的全面认知:面试官不仅仅是想让你列几条不同点,更是想知道你是否理解两种锁的设计哲学——
synchronized是 JVM 层面的 "内建锁",ReentrantLock是 API 层面的 "显式锁"。这两条路线的差异,决定了它们在功能、性能和使用方式上的根本不同。 -
实战选型能力:考察你在实际项目中能否根据业务场景选择合适的锁。比如需要公平锁、需要尝试获取锁、需要多个条件变量时,用
synchronized就搞不定。 -
底层原理深度:如果你能说出
synchronized的锁升级过程(偏向锁 → 轻量级锁 → 重量级锁),以及ReentrantLock的 AQS 原理,面试官会认为你是真正理解了,而不是在背八股。
核心答案
先甩一张总览表:
| 对比维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 关键字(内建锁) | JDK API(java.util.concurrent.locks) |
| 加锁/解锁方式 | 自动(进入同步块加锁,退出自动释放) | 手动(lock() / unlock(),必须在 finally 中释放) |
| 可中断性 | 不可中断(等待锁时无法响应中断) | 可中断(lockInterruptibly()) |
| 公平性 | 只有非公平锁 | 支持公平锁和非公平锁(构造参数控制) |
| 条件变量 | 只有 wait() / notify(),一个等待队列 | 支持 Condition,多个等待队列 |
| 尝试获取锁 | 不支持(拿不到就一直阻塞等) | 支持(tryLock(),拿不到立刻返回 false) |
| 超时获取锁 | 不支持 | 支持(tryLock(timeout, unit)) |
| 锁绑定条件 | 只有一个条件 | 多个 Condition,精确唤醒 |
| 可重入性 | ✅ 可重入 | ✅ 可重入 |
| 性能 | JDK 6 之后经过锁升级优化,性能不差 | 高并发下略优,功能更灵活 |
简单记:synchronized 是 "自动挡",简单省心;ReentrantLock 是 "手动挡",灵活但需要自己控制。
深度解析
一、基本用法对比
// ============ synchronized 用法 ============
public class SyncDemo {
// 修饰实例方法(锁是当前实例对象)
public synchronized void method1() {
// 业务逻辑
}
// 修饰静态方法(锁是当前类的 Class 对象)
public static synchronized void method2() {
// 业务逻辑
}
// 修饰代码块(锁是括号里的对象)
public void method3() {
synchronized (this) {
// 业务逻辑
}
}
}
// ============ ReentrantLock 用法 ============
public class LockDemo {
private final ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
public void method() {
lock.lock(); // 手动加锁
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须在 finally 中释放!忘了就死锁了
}
}
}
注意看区别:synchronized 不需要手动释放锁,JVM 帮你搞定;ReentrantLock 必须在 finally 中 unlock(),忘记释放锁是一个常见的生产事故来源。这点在面试中提到,会显得你很有实战意识。
二、ReentrantLock 的四大高级能力
synchronized 搞不定的场景,才是 ReentrantLock 的主战场。
1. 尝试获取锁——避免死锁
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
// tryLock() 拿不到锁立刻返回 false,不会阻塞等待
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
// 两把锁都拿到了,执行业务逻辑
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
// 拿不到就做别的事,不会死等,有效避免死锁
这种 "拿不到就走" 的模式,用 synchronized 根本做不到。synchronized 一旦进入等待,就只能阻塞,什么都干不了。
2. 超时获取锁——防止无限等待
// 等待 3 秒,拿不到就放弃
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} finally {
lock.unlock();
}
} else {
// 超时未获取到锁,执行降级逻辑
log.warn("获取锁超时,执行降级方案");
}
这个在防止系统雪崩时特别有用——如果大量线程都在等同一把锁,超时机制能快速失败,不至于把线程池打满。
3. 可中断获取锁——优雅响应中断
// lockInterruptibly() 在等待锁的过程中可以响应中断
try {
lock.lockInterruptibly();
try {
// 业务逻辑
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 被中断了,做清理工作
Thread.currentThread().interrupt();
}
synchronized 在等待锁时是无法响应中断的。如果你需要优雅地停止一个正在等锁的线程,只能用 ReentrantLock。
4. Condition 精确唤醒——多个等待队列
上图对比了两种锁的唤醒机制,核心区别在于:
-
synchronized 只有一个等待队列,调用
notify()时随机唤醒一个线程,调用notifyAll()时全部唤醒。问题是——如果你有两种不同条件的等待线程混在一个队列里,根本没法精确唤醒 "等条件 A" 的线程。你只能notifyAll(),把所有人都叫醒,不符合条件的再接着等。这就是所谓的 "惊群效应"。 -
ReentrantLock 支持多个
Condition,每个Condition是独立的等待队列。你可以精确地只唤醒 "等某个条件" 的线程。生产者-消费者模型中,这特别好用。
// 经典的生产者-消费者模型(Condition 精确唤醒)
public class BoundedBuffer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 条件 1:不满
private final Condition notEmpty = lock.newCondition(); // 条件 2:不空
private final Object[] items = new Object[100];
private int putIdx, takeIdx, count;
// 生产者:往队列里放元素
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 队列满了,等待 "不满" 条件
items[putIdx] = x;
if (++putIdx == items.length) putIdx = 0;
count++;
notEmpty.signal(); // 精确唤醒等待 "不空" 条件的消费者
} finally {
lock.unlock();
}
}
// 消费者:从队列里取元素
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 队列空了,等待 "不空" 条件
Object x = items[takeIdx];
if (++takeIdx == items.length) takeIdx = 0;
count--;
notFull.signal(); // 精确唤醒等待 "不满" 条件的生产者
return x;
} finally {
lock.unlock();
}
}
}
三、底层实现原理
两者底层走的是完全不同的路线:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现 | JVM 层面(字节码指令 monitorenter / monitorexit) | API 层面(基于 AQS) |
| 核心 | 对象头中的 Mark Word 和 Monitor | AQS 的 state 变量 + CLH 队列 |
| 优化 | 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁 | CAS + LockSupport.park()/unpark() |
synchronized 在 JDK 6 之前是纯重量级锁(依赖操作系统的 Mutex),性能确实差。但 JDK 6 引入了锁升级机制后,大部分场景下性能已经和 ReentrantLock 差不多了。所以 "ReentrantLock 性能更好" 这个说法已经过时了,面试中别踩这个坑。
面试高频追问
-
追问一:synchronized 的锁升级过程了解吗?
JDK 6 之后,
synchronized引入了锁升级机制:无锁 → 偏向锁(只有一个线程访问时,直接把线程 ID 记录在对象头的 Mark Word 中)→ 轻量级锁(多个线程交替访问时,用 CAS 自旋获取锁)→ 重量级锁(真正竞争激烈时,膨胀为操作系统的 Mutex 锁,线程阻塞)。这个升级是单向的,不可降级(实际上重量级锁在特定条件下可以降级,但 STW 场景才发生,可以忽略)。 -
追问二:什么是公平锁和非公平锁?ReentrantLock 默认是哪种?
公平锁按线程等待的先后顺序获取锁(先到先得),非公平锁允许插队。
ReentrantLock默认是 非公平锁(new ReentrantLock()等价于new ReentrantLock(false))。非公平锁性能更好,因为减少了线程上下文切换,但可能导致线程 "饥饿"。实际开发中绝大部分场景用非公平锁就够了。 -
追问三:AQS 是什么?
AQS(AbstractQueuedSynchronizer)是
java.util.concurrent的核心框架。它内部维护一个volatile int state变量表示锁状态,以及一个 CLH 双向队列管理等待线程。ReentrantLock、Semaphore、CountDownLatch等都是基于 AQS 实现的。
常见面试变体
- "什么场景下用 ReentrantLock 比 synchronized 更合适?"
- "synchronized 底层是怎么实现的?"
- "什么是可重入锁?synchronized 是可重入锁吗?"
- "公平锁和非公平锁有什么区别?"
记忆口诀
选锁口诀:简单同步用 synchronized(自动挡、省心),需要高级功能用 ReentrantLock(手动挡、灵活)。高级功能指:尝试获取、超时获取、可中断、多条件队列。
总结
synchronized 和 ReentrantLock 不是谁替代谁的关系,而是互补的。简单场景用 synchronized(代码简洁、不会忘记释放锁),需要尝试获取、超时获取、可中断、多个条件队列时用 ReentrantLock。面试中把 "自动挡 vs 手动挡" 的比喻说出来,再配合 AQS 和锁升级的原理,基本稳了。