synchronized 和 ReentrantLock 有什么区别?
简化版
synchronized 是 JVM 内置的监视器锁,写法简单、出块/异常时自动释放;ReentrantLock 是基于 AQS 的 JDK 类锁,功能更强——支持可中断、超时获取、公平锁、多个条件队列,但必须在 finally 里手动 unlock()。没有特殊需求优先用 synchronized。
详细版
两者都保护临界区(同一时刻只有一个线程进),也都可重入(同一线程能重复获取自己已持有的锁,不会自锁)。区别在功能和用法:
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 层面 | JVM 关键字、监视器锁 | JDK 类,底层基于 AQS |
| 释放 | 出同步块/抛异常自动释放 | 必须显式 unlock() |
| 等锁时可中断 | 不支持 | lockInterruptibly() 支持 |
| 超时获取 | 不支持 | tryLock(timeout) 支持 |
| 公平性 | 只能非公平 | 可选公平/非公平 |
| 条件队列 | 一组 wait/notify | 一个锁可建多个 Condition |
// synchronized:只锁真正要保护的那几行
synchronized (lock) {
balance -= amount;
}
// ReentrantLock:finally 里 unlock 是「正确性要求」不是习惯
Lock lock = new ReentrantLock();
lock.lock();
try {
updateSharedState();
} finally {
lock.unlock();
}
完整版教学
一、为什么默认选 synchronized
「synchronized 慢、ReentrantLock 快」是过时结论。现代 HotSpot 会先走轻量级锁路径,竞争加剧时再膨胀为重量级监视器;偏向锁已在 JDK 15 默认禁用,并于后续版本移除。它最大的工程优势是自动释放:退出同步块或抛异常时,JVM 都会释放监视器。
反观 ReentrantLock,如果 unlock() 漏写或没有放进 finally,异常路径可能让锁长期得不到释放,后续竞争线程持续阻塞。所以能力越大责任越大——没用到 ReentrantLock 的高级功能,就不必额外承担手动管理风险。
二、ReentrantLock 到底解决哪些 synchronized 做不到的事
选 ReentrantLock 应该是「我需要某个具体能力」,而不是「它听起来更高级」:
- 可中断等待
lockInterruptibly():线程在等锁时能响应中断、放弃等待——适合需要能取消的任务,避免死等。 - 超时获取
tryLock(2, SECONDS):等不到就返回,可用来避免死锁(拿不到第二把锁就回退重试)。 - 公平锁
new ReentrantLock(true):按排队顺序给锁,避免线程饥饿(代价是吞吐下降)。synchronized 只能非公平。 - 多个条件队列
Condition:生产者-消费者里,可以分别建「队列非满」和「队列非空」两个 Condition,精确唤醒对应线程,而 synchronized 的notifyAll会把所有等待线程都吵醒。
三、两个必须知道的用法陷阱
① tryLock 失败不能 unlock:
if (lock.tryLock()) {
try { ... } finally { lock.unlock(); } // 只有拿到锁才 unlock
}
// tryLock 返回 false 时若也 unlock,会抛 IllegalMonitorStateException
② synchronized 的锁对象要私有且稳定:
// 避免用 this、字符串常量或可被外部拿到的对象当锁,否则可能与无关代码竞争同一把锁
private final Object lock = new Object(); // 私有 final 专用锁对象
字符串常量在常量池里全局共享,用它当锁可能和完全不相干的代码撞锁,引发诡异的死锁或阻塞。
四、锁不是越多越安全
线程安全还取决于锁粒度和临界区内容:
- 别把 RPC、数据库、磁盘 I/O 放进锁里——会把并发拖成串行,吞吐暴跌;
- 但也别为了「锁得少」把一个必须整体完成的「校验+写入」拆到锁外,那会产生竞态。
- 单变量累加优先用
AtomicLong(CAS 无锁);读多写少用ReadWriteLock/StampedLock;最稳的设计是减少共享可变状态,而不是无止境加锁。
五、内存语义相同,能力边界不同
对正确同步而言,释放 Lock 与随后获取同一把 Lock 具有与监视器解锁/加锁相同类型的内存同步效果。二者都能保护复合不变量,差别主要在控制能力与使用方式,而不是“一个可见、一个不可见”。
| 需求 | synchronized | ReentrantLock |
|---|---|---|
| 自动释放 | 退出语句块自动完成 | 必须 finally unlock() |
| 可中断等锁 | 进入监视器等待不可直接中断 | lockInterruptibly() |
| 超时/尝试 | 不提供 tryLock API | tryLock()/带超时重载 |
| 多条件队列 | 每个监视器一个 wait set | 可创建多个 Condition |
| 公平策略 | 无配置开关 | 构造时可选公平锁 |
公平锁也不是严格实时调度保证,tryLock() 等路径还有各自语义。选择 API 前应先明确是否真正需要可中断、超时或多个条件队列。
六、用等待预算理解为什么需要 tryLock
假设请求总超时 300 ms,获取锁最多只能占 50 ms,剩余 250 ms 留给数据库与响应。tryLock(50, MILLISECONDS) 失败后可以快速降级,而 synchronized 无法直接表达这个等锁预算。
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
updateState();
} finally {
lock.unlock();
}
} else {
rejectOrDegrade();
}
超时不等于逻辑自动安全:失败分支要保证没有写入一半,成功分支必须在 finally 释放。若临界区内又调用慢远程服务,即使锁获取有超时,持锁时间仍可能失控。
记忆钩子:默认先选结构简单、自动释放的 synchronized;只有明确需要“可中断、可超时、公平策略、多 Condition”时,ReentrantLock 的额外复杂度才有价值。
七、常见误区与追问
- 误区:ReentrantLock 因为是 JDK 类,所以一定比 synchronized 快。 性能取决于 JDK、竞争和临界区,选型应先看语义能力与实测。
- 误区:调用
lock()后可以在任意位置随手unlock()。 获取成功后必须用紧邻的 try-finally 配对,否则异常会永久占锁。 - 误区:公平锁保证每个线程严格按到达时间执行。 它倾向按队列顺序授予锁,但调度和特定 API 路径不构成硬实时承诺。
- 追问:为什么 synchronized 更不容易泄漏锁? JVM 在正常和异常退出同步区域时自动释放监视器,不依赖业务代码手工调用。
- 追问:
lockInterruptibly()解决什么问题? 线程在等锁期间可响应中断,便于取消任务或打破长等待链。 - 追问:多个 Condition 有什么价值? 可把“非空”“未满”等不同条件的等待者分组,减少无关唤醒。
- 追问:二者都可重入吗? 是,同一线程可重复进入,但必须匹配相同次数的退出或 unlock。
八、加强记忆
两者都提供互斥、可见性与可重入,区别在“自动语法”与“显式控制”:synchronized 结构化、异常自动释放,适合绝大多数简单临界区;ReentrantLock 基于 AQS,需要 finally 解锁,却换来可中断等待、tryLock 超时、公平选项和多个 Condition。不要用“谁绝对更快”选型,而要先问业务是否有锁等待预算、取消需求和多条件协调。无论选哪种,缩短临界区、避免锁内 I/O 和保持锁对象/锁实例稳定都比换 API 更重要。