synchronized 和 ReentrantLock 的区别
对比 synchronized 与 ReentrantLock 的实现、可重入、公平锁、中断、超时、Condition 和适用场景。
synchronized和reentrantLock的区别?
面试标准回答
synchronized 是 JVM 层面的内置锁,语法简单,进入同步块后自动加锁,退出时自动释放,支持可重入和内存可见性。ReentrantLock 是基于 AQS 实现的显式锁,同样支持可重入,但提供了公平锁、可响应中断获取锁、tryLock、超时获取以及多个 Condition 等高级能力。两者在现代 JDK 中性能差距通常不是主要选型依据;功能简单时优先 synchronized,需要精细控制时使用 ReentrantLock,并且必须在 finally 中释放锁。
一句话记忆:
synchronized 简单自动,ReentrantLock 灵活可控。
详细讲解
synchronized 和 ReentrantLock 都能实现互斥和可见性,但定位不同:
synchronized是 JVM 原生关键字,语法简单;ReentrantLock是 JUC 提供的显式锁,功能更丰富、控制能力更强。
一、基本用法
synchronized
synchronized (lock) {
// 临界区
}
或者:
public synchronized void method() {
}
锁会自动释放。
ReentrantLock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
必须手动释放,所以通常必须写在 finally 中。
二、核心区别
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 实现层次 | JVM 关键字、Monitor | Java 类,基于 AQS |
| 加锁释放 | 自动 | 手动 |
| 可重入 | 支持 | 支持 |
| 公平锁 | 不支持显式配置 | 支持 |
| 可中断获取锁 | 不支持 | lockInterruptibly() |
| 尝试获取锁 | 不支持 | tryLock() |
| 超时获取锁 | 不支持 | tryLock(timeout, unit) |
| 条件队列 | 一个 Monitor WaitSet | 可创建多个 Condition |
| 等待通知 | wait/notify/notifyAll | await/signal/signalAll |
| 锁状态查询 | 能力有限 | 提供较多查询方法 |
| 编码复杂度 | 低 | 较高 |
| 异常释放 | 自动释放 | 必须 finally unlock |
三、两者都支持可重入
可重入指:
同一个线程已经持有锁时,可以再次获取同一把锁。
synchronized
public synchronized void methodA() {
methodB();
}
public synchronized void methodB() {
}
同一个对象上的两个同步方法,线程可以重入。
ReentrantLock
lock.lock();
try {
lock.lock();
try {
// 重入
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
获取几次,就必须释放几次。
四、ReentrantLock 支持公平锁
ReentrantLock fairLock = new ReentrantLock(true);
公平锁会尽量按 AQS 队列顺序获取锁。
默认是非公平锁:
ReentrantLock lock = new ReentrantLock();
非公平锁允许新线程插队,吞吐量通常更高。
synchronized 没有 API 让你指定公平性,通常按非公平竞争理解。
五、ReentrantLock 支持可响应中断等待
假设线程正在等待锁。
使用:
lock.lock();
即使线程被中断,也不会因为中断立即退出获取锁过程。
而:
lock.lockInterruptibly();
等待期间如果收到中断,会抛出:
InterruptedException
例如:
try {
lock.lockInterruptibly();
try {
doWork();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
这对于避免线程长时间死等、响应取消请求很有价值。
六、ReentrantLock 支持尝试获取和超时
立即尝试
if (lock.tryLock()) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
// 获取失败,走降级逻辑
}
超时尝试
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
// 两秒内未拿到锁
}
synchronized 一旦进入竞争,只能等待,无法直接设置超时。
这也是 ReentrantLock 在需要降级、超时控制时的重要优势。
七、Condition 比 wait/notify 更灵活
synchronized 的一个 Monitor 只有一个 WaitSet。
例如阻塞队列里:
生产者等待 notFull
消费者等待 notEmpty
使用 synchronized 时,生产者和消费者都在同一个 WaitSet 中,通常要:
notifyAll();
而 ReentrantLock 可以创建多个 Condition:
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
生产者等待:
while (queue.size() == capacity) {
notFull.await();
}
消费者等待:
while (queue.isEmpty()) {
notEmpty.await();
}
生产完成:
notEmpty.signal();
消费完成:
notFull.signal();
这样可以精确唤醒,减少无效唤醒。
八、底层实现不同
synchronized
经典理解:
对象头 Mark Word
+
Monitor
+
JVM 锁优化
JDK 8 中可能经历:
偏向锁
→ 轻量级锁
→ 重量级锁
这是 HotSpot 的 JVM 实现优化。
ReentrantLock
主要基于:
AbstractQueuedSynchronizer
也就是 AQS。
核心状态:
volatile int state;
独占锁时:
state = 0 未加锁
state = 1 第一次获取
state > 1 重入次数
竞争失败的线程进入 CLH 变体同步队列,并通过 park/unpark 等待和唤醒。
九、异常情况下的差异
synchronized:
synchronized (lock) {
throw new RuntimeException();
}
离开同步块时,JVM 自动释放锁。
ReentrantLock:
lock.lock();
doWork();
lock.unlock();
如果 doWork() 抛异常:
unlock 没执行
→ 锁永久未释放
所以必须写:
lock.lock();
try {
doWork();
} finally {
lock.unlock();
}
这是 ReentrantLock 最常见的编码风险。
十、性能区别
早期 JDK 中,ReentrantLock 性能常常明显优于 synchronized。
但 JDK 6 以后,JVM 对 synchronized 做了大量优化:
- 偏向锁
- 轻量级锁
- 自旋
- 锁消除
- 锁粗化
现代 JDK 中:
两者性能差异通常不是选型的首要依据。
应该根据功能需求选择,而不是简单认为 ReentrantLock 一定更快。
十一、什么时候使用 synchronized
适合:
- 临界区简单
- 不需要公平锁
- 不需要超时获取
- 不需要中断锁等待
- 只有一个等待条件
- 希望代码简单、自动释放锁
例如:
public synchronized void increment() {
count++;
}
一般优先原则:
能用 synchronized 清晰表达时,优先用 synchronized。
十二、什么时候使用 ReentrantLock
适合:
- 需要公平锁
- 需要
tryLock - 需要超时获取锁
- 需要可中断等待
- 需要多个 Condition
- 需要查询等待队列、持锁状态
- 需要更精细的锁控制
例如转账时避免死锁:
if (accountA.lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (accountB.lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
transfer();
} finally {
accountB.lock.unlock();
}
}
} finally {
accountA.lock.unlock();
}
}
使用 synchronized 很难实现这种超时退避。
十三、wait/notify 和 await/signal 对应关系
| synchronized | ReentrantLock |
|---|---|
wait() | Condition.await() |
notify() | Condition.signal() |
notifyAll() | Condition.signalAll() |
两者都要求:
调用等待或通知方法前,必须先持有对应的锁。
并且等待都应该放在 while 中:
while (!conditionSatisfied()) {
condition.await();
}
防止虚假唤醒和竞争后条件失效。
十四、常见误区
误区一:ReentrantLock 才支持可重入
不对。两者都可重入。
误区二:ReentrantLock 一定比 synchronized 快
不对。现代 JVM 下要看场景,功能差异比纯性能更重要。
误区三:synchronized 会自动释放,ReentrantLock 不会
更准确地说:
- synchronized 离开同步块时自动释放
- ReentrantLock 必须显式 unlock
误区四:公平锁一定更好
公平锁吞吐量通常更低,只在有明确公平性需求时使用。
误区五:tryLock 失败后可以直接 unlock
不可以。只有成功获取锁后才能释放。
正确写法:
boolean locked = false;
try {
locked = lock.tryLock();
if (!locked) {
return;
}
doWork();
} finally {
if (locked) {
lock.unlock();
}
}
评论与讨论
回复 :
留下你的想法