以下是 ReentrantLock 与 synchronized 的全面对比解析,涵盖实现机制、特性差异、性能演进、选型指南及代码示例。
ReentrantLock vs synchronized 完全对比手册
一、核心实现机制对比
| 维度 | synchronized |
ReentrantLock |
|---|---|---|
| 实现层级 | JVM 内置(C++ 实现,依赖对象头 Mark Word 和 Monitor) | JDK 层 API(Java 实现,基于 AQS 框架) |
| 锁状态存储 | 对象头中的 Mark Word(记录锁状态、持有线程、重入次数等) | AQS 的 volatile int state 字段 |
| 等待队列 | 每个对象关联一个 Monitor ,内部维护 _cxq、_EntryList、_WaitSet 三个队列 |
AQS 内部维护一个 FIFO 的 CLH 变种队列 |
| 条件变量 | 单个隐式条件队列(wait/notify/notifyAll) |
支持多个 Condition 对象,每个维护独立条件队列 |
| 锁升级机制 | 偏向锁 → 轻量级锁 → 重量级锁(JDK 6+ 优化) | 无锁升级,直接基于 CAS 和队列实现重量级互斥 |
| 释放方式 | 自动释放(代码块结束或异常) | 必须显式释放 (unlock() 通常在 finally 中) |
二、特性功能全面对比
| 功能特性 | synchronized |
ReentrantLock |
说明 |
|---|---|---|---|
| 可重入性 | ✅ 支持 | ✅ 支持 | 同一线程可重复获取已持有的锁 |
| 公平锁 | ❌ 仅非公平 | ✅ 构造函数指定 true 为公平锁 |
公平锁保证 FIFO 顺序,但吞吐量低 |
| 可中断获取 | ❌ 阻塞时不可中断 | ✅ lockInterruptibly() |
等待锁时可响应 Thread.interrupt() |
| 超时获取 | ❌ 不支持 | ✅ tryLock(timeout, unit) |
允许在指定时间内尝试获取锁 |
| 非阻塞尝试 | ❌ 不支持 | ✅ tryLock() |
立即返回,不排队等待 |
| 多条件等待 | ❌ 仅单个隐式条件 | ✅ 多个 Condition |
可实现精确唤醒,避免惊群效应 |
| 状态监控 | ❌ 无直接 API | ✅ getQueueLength()、hasQueuedThreads() 等 |
便于运维监控和性能调优 |
| 锁降级/升级 | ❌ 不支持 | ❌ 不支持 | 两者均不直接支持锁降级 |
三、底层源码实现对比
1. synchronized 底层(JVM 层面)
cpp
// 简化示意:synchronized 代码块编译后对应 monitorenter / monitorexit 指令
// 实际 JVM 实现涉及 ObjectMonitor 结构
class ObjectMonitor {
_header = NULL; // 对象头备份
_count = 0; // 重入次数
_waiters = 0; // 等待线程数
_recursions = 0; // 递归次数(同 count)
_owner = NULL; // 持有线程
_WaitSet = NULL; // wait() 线程队列
_EntryList = NULL; // 阻塞等待锁的线程队列
...
};
// 进入同步块:monitorenter
// 1. 如果当前对象无锁(偏向/轻量级未膨胀),尝试 CAS 获取偏向锁/轻量级锁
// 2. 失败则膨胀为重量级锁,进入 _EntryList 等待
// 3. 重入时直接递增 _recursions
// 退出同步块:monitorexit
// 1. 递减 _recursions,若归零则释放锁
// 2. 若 _EntryList 不为空,唤醒队首线程
2. ReentrantLock 底层(AQS 层面)
java
// 核心:基于 AQS 的 state 变量和 CLH 队列
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 重入计数 state
// state == 0: 锁空闲
// state > 0: 锁被持有,值为重入次数
// 释放锁钩子
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
// 非公平锁获取逻辑
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) // 第一次插队
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 进入 AQS 排队
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); // 第二次插队机会
}
}
}
四、性能演进与现状
| JDK 版本 | synchronized 性能状态 |
ReentrantLock 性能状态 |
建议 |
|---|---|---|---|
| JDK 1.5 之前 | 纯重量级锁,每次加锁都涉及系统调用,性能极差 | 尚未引入(java.util.concurrent 包在 1.5 发布) |
无选择余地 |
| JDK 1.5 | 重量级锁 | 刚推出,基于 AQS,性能远优于 synchronized |
优先使用 ReentrantLock |
| JDK 1.6 | 引入偏向锁、轻量级锁、自旋锁优化,性能大幅提升 | 稳定,性能优秀 | 两者性能接近,按功能需求选择 |
| JDK 1.8+ | 持续优化(如锁消除、锁粗化),低竞争下几乎无开销 | 性能稳定 | 低竞争无差别 ,高竞争 ReentrantLock 略优(但差距很小) |
当前结论 :在大多数常见场景下,两者性能处于同一量级 ,不应再将性能作为首选区分因素。应基于功能需求 和代码安全性做出选择。
五、代码风格与安全性对比
| 对比点 | synchronized |
ReentrantLock |
|---|---|---|
| 代码简洁性 | ⭐⭐⭐⭐⭐ 无需手动释放,代码块即锁范围 | ⭐⭐ 必须显式 lock()/unlock() |
| 异常安全性 | ⭐⭐⭐⭐⭐ 自动释放,无死锁风险 | ⭐⭐⭐ 若忘记 finally 释放,死锁 |
| 灵活控制 | ⭐⭐ 无法中断、超时、非阻塞 | ⭐⭐⭐⭐⭐ 提供多种锁获取方式 |
| 条件同步 | ⭐⭐ 只能 wait/notify 在单个对象上 |
⭐⭐⭐⭐⭐ 多 Condition 精准控制 |
| 可调试性 | ⭐⭐⭐ 难以查询锁状态 | ⭐⭐⭐⭐ 提供 getQueueLength() 等监控 API |
示例对比:
java
// synchronized 风格 ------ 简单安全
synchronized (lock) {
doSomething();
}
// ReentrantLock 风格 ------ 灵活但需谨慎
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
六、典型场景选型指南
优先使用 synchronized 的场景
- 简单互斥:仅需保护临界区,无特殊功能要求。
- 方法级同步 :直接在方法上使用
synchronized关键字。 - 团队规范 :团队对
ReentrantLock不熟悉,易用错导致死锁。 - 性能非瓶颈:临界区执行时间极短,锁竞争不激烈。
必须使用 ReentrantLock 的场景
| 场景 | 所需特性 | 示例 |
|---|---|---|
| 需要超时放弃 | tryLock(timeout) |
获取数据库连接,等待 500ms 无果则降级返回缓存 |
| 需要响应中断 | lockInterruptibly() |
用户点击取消,立即终止等待锁的线程 |
| 需要公平锁 | new ReentrantLock(true) |
任务调度系统严格按提交顺序执行 |
| 需要多条件精准唤醒 | 多个 Condition |
有界阻塞队列,生产者/消费者分别唤醒对方 |
| 需要监控锁状态 | getQueueLength() 等 |
运维监控系统,告警等待线程数过多 |
| 复杂锁交互 | 非阻塞尝试 tryLock() |
避免死锁,尝试获取多个锁 |
七、混合使用注意事项
- 不要混用:同一个共享资源的同步不要混用两种锁机制,极易出错。
synchronized不可与Condition配合 :Condition必须与Lock绑定。wait/notify必须在synchronized块内 ,而await/signal必须在Lock块内。
八、总结对比表(一图看懂)
text
┌─────────────────────┬────────────────────────────┬────────────────────────────┐
│ 维度 │ synchronized │ ReentrantLock │
├─────────────────────┼────────────────────────────┼────────────────────────────┤
│ 实现层 │ JVM (C++) │ JDK (Java) │
│ 释放方式 │ 自动 │ 手动(必须 finally) │
│ 公平锁 │ ❌ │ ✅ │
│ 可中断获取 │ ❌ │ ✅ │
│ 超时获取 │ ❌ │ ✅ │
│ 非阻塞尝试 │ ❌ │ ✅ │
│ 多条件变量 │ ❌ (单个 wait/notify) │ ✅ (多个 Condition) │
│ 状态监控 │ ❌ │ ✅ │
│ 性能(低竞争) │ 相当 │ 相当 │
│ 性能(高竞争) │ 相当(JVM 持续优化) │ 略优(但差距已极小) │
│ 代码复杂度 │ 低 │ 中~高 │
│ 适用场景 │ 大多数普通同步需求 │ 需要高级控制功能的复杂场景 │
└─────────────────────┴────────────────────────────┴────────────────────────────┘
九、最终建议
默认首选
synchronized------ 简洁、安全、不易出错,满足 90% 的同步需求。当且仅当需要以下特性时,才升级到
ReentrantLock:
- 需要超时、可中断、非阻塞的锁获取
- 需要公平锁保证顺序
- 需要多个条件变量实现精准线程调度
- 需要监控锁的运行时状态
记住:简单的代码往往更可靠 。不要为了"炫技"而滥用 ReentrantLock。