synchronized 和 ReentrantLock 有什么区别?项目里该怎么选
`synchronized` 和 `ReentrantLock` 的区别不难背,难的是放回项目代码里时到底该为哪些能力多付复杂度。把简单互斥、超时、中断、Condition 这些真实需求拆开,选型会比直接背特点更靠谱。
synchronized 和 ReentrantLock 是 Java 并发里最常被放在一起比较的一组工具。只要聊到线程安全、锁竞争、并发控制,几乎都会有人问:它们到底有什么区别,项目里该用哪个。很多人也知道一些常见答案,比如:
ReentrantLock更灵活synchronized写法更简单ReentrantLock可以中断、可以超时synchronized是 JVM 关键字
这些说法都不算错,但如果只是停留在这个层面,到了真实项目里还是不一定知道怎么选。因为工程上真正有价值的问题不是“背出它们的特点”,而是:
当前这个并发问题,到底只是要一个简单可靠的互斥,还是需要更复杂的锁控制能力?
我想讲清的不是面试题答案,而是项目里到底要不要为 tryLock、超时、中断、Condition 这些能力多承担一层心智负担。很多场景里,锁的能力越多不等于越划算。
这篇讨论的是本地锁选型
先用这张表把范围收住,别把本地锁选择和别的并发问题揉在一起。
| 你现在看到的现象 | 更像什么问题 | 更值得先看什么 |
|---|---|---|
你在写并发控制代码,想判断 synchronized 还是 ReentrantLock 更合适 | 本地锁选型 | 直接往下看这篇 |
| 你更想梳理项目里常见的线程安全坑,而不只是一把锁 | 并发基础问题 | 去看 Java 项目里最常见的线程安全问题有哪些 |
| 你主要在处理 ThreadLocal 串用、线程复用和上下文边界 | ThreadLocal 边界问题 | 接 ThreadLocal 常见坑有哪些?项目里最容易踩的几个问题 |
| 你已经进入分布式协调,多节点竞争是核心问题 | 分布式锁选型 | 接 分布式锁什么时候该用,什么时候不该用 |
| 现象已经表现为线程池堆积、接口变慢或执行链拥塞 | 故障排查主线 | 接 线程池打满以后,应该先查队列、拒绝策略还是慢任务? |
讨论范围:JVM 内的本地互斥
这里讨论的是 JVM 进程内的本地锁怎么选。
如果你手上的问题其实是:
- 共享状态和复合操作到底哪里不安全
- ThreadLocal 在线程复用下怎么串值
- 多节点竞争要不要上分布式锁
- 线程池、接口 RT、CPU 已经开始一起变差
那就不该只停在这里。
读这篇更合适的前提,是你已经确认自己面对的是本地临界区互斥,现在只差把 synchronized 和 ReentrantLock 选明白。
一、先有一个总认识:它们解决的核心问题是一样的
先别被写法和 API 形式带偏。
synchronized 和 ReentrantLock 虽然一个是关键字,一个是类,但它们最核心的目标其实是一样的:
- 在多线程环境下
- 保护共享可变状态
- 让同一时刻只有有限线程进入临界区
也就是说,它们都属于“互斥控制工具”。
所以真正的区别不在于“谁能加锁谁不能”
而在于:
- 锁的获取方式有多灵活
- 失败和等待时能不能做更多控制
- 用起来的复杂度和出错成本有多高
先把这个大前提抓住,后面就不会把它们想成两种完全不同的问题解法。
二、第一层区别:synchronized 更直接,ReentrantLock 更显式
最表面的差异就是写法。
synchronized 通常这样写
synchronized (lock) {
doSomething();
}
或者:
public synchronized void update() {
// do something
}
ReentrantLock 通常这样写
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
这层差异意味着什么
synchronized 的好处是:
- 语法短
- 结构直观
- 退出代码块时 JVM 自动释放锁
- 不容易因为忘记释放锁而出事故
ReentrantLock 的好处是:
- 所有动作都更显式
- 你能更明确地控制什么时候拿锁、什么时候释放锁
- 也能在锁获取失败、超时、中断时做更多处理
但代价是:
- 代码更啰嗦
- 必须自己保证
unlock()在finally里执行 - 写错更容易出问题
三、第二层区别:两者都可重入,但可扩展能力不同
很多人会先记住一个词:可重入。
什么叫可重入
简单说就是:
- 同一个线程已经拿到了这把锁
- 之后再次进入同一把锁保护的代码时
- 不会把自己锁死
这一点上:
synchronized可重入ReentrantLock也可重入
所以“可重入”不是它们的主要分界线。
真正的区别在于
ReentrantLock 在“锁已经被占用后,接下来怎么办”这件事上,给了你更多选择。
四、ReentrantLock 的一个关键优势:可以尝试获取锁,而不是只能傻等
synchronized 的行为比较直接:
- 锁被别人占了
- 当前线程就阻塞等待
而 ReentrantLock 可以做更多事情,例如:
1. 试着拿锁,不成功就算了
if (lock.tryLock()) {
try {
doSomething();
} finally {
lock.unlock();
}
}
2. 等一段时间,超时还拿不到就放弃
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
doSomething();
} finally {
lock.unlock();
}
}
为什么这个能力在项目里有用
因为有些场景下,你并不想让线程无限期傻等。
例如:
- 某个任务抢不到锁就稍后重试
- 某个接口不希望一直卡住等待锁
- 某个后台任务超时后直接跳过本轮
这种时候,ReentrantLock 的灵活性就明显更强。
五、ReentrantLock 还能响应中断,这对某些线程协作场景很重要
synchronized 阻塞等待锁时,控制相对更硬。ReentrantLock 则支持:
lock.lockInterruptibly();
这意味着:
- 线程在等锁时
- 可以被中断
- 然后及时退出等待
这适合什么场景
- 后台任务取消
- 线程池任务超时退出
- 某些更复杂的并发控制和线程协作逻辑
但也别高估这个特性
大多数普通业务代码里,其实并不需要这么细的控制。
所以这个能力很强,但它不是“凡是写锁都该优先用 ReentrantLock”的理由。它更像是:
- 当你明确需要时,
ReentrantLock才显得特别有价值
六、ReentrantLock 可以选公平锁,但公平不一定更好
ReentrantLock 支持公平锁和非公平锁。
公平锁的意思是
- 大致按等待顺序拿锁
- 先来的线程更优先
非公平锁的意思是
- 不严格按排队顺序
- 某些线程可能插队拿到锁
公平锁听起来很美好,但为什么项目里不一定常用
因为公平通常意味着:
- 调度成本更高
- 吞吐不一定最好
很多业务系统更看重整体吞吐和响应效率,而不是严格排队公平。所以实际项目里,默认非公平锁往往更常见。
这也说明一个工程事实:
- 锁能力更强,不代表默认就更适合所有场景
七、synchronized 的最大优点其实不是“老”,而是简单且不容易用错
聊到这里,很多人容易把 ReentrantLock 理解成“更高级”,把 synchronized 理解成“简单但落后”。这其实不准确。
synchronized 真正的优势在于
- 语义直接
- 代码短
- JVM 自动释放锁
- 出错面更小
- 绝大多数简单互斥场景已经够用
对于很多项目代码来说,你真正需要的只是:
- 一个简单的临界区保护
- 不需要超时、不需要中断、不需要多条件队列
这种时候,synchronized 往往反而是更好的选择。
为什么
因为最好的并发工具,不是能力最多的,而是:
- 能满足需求
- 同时最不容易写错的那个
八、ReentrantLock 真正更适合什么场景
如果你明确碰到下面这些需求,ReentrantLock 通常会更顺手:
1. 需要尝试获取锁,拿不到就走别的分支
例如:
- 抢任务
- 非阻塞尝试更新
- 避免接口长时间傻等
2. 需要可中断的锁等待
例如:
- 可取消任务
- 更复杂的线程协作
3. 需要多个条件队列
ReentrantLock 可以配合 Condition 做更细的等待/唤醒控制,这比 synchronized + wait/notify 更清晰一些。
4. 需要更显式的锁状态管理
例如想知道:
- 当前是否被锁住
- 是否有线程在等待
- 是否需要按某种策略决定加锁行为
这些时候,ReentrantLock 的 API 会更有表达力。
九、项目里更实际的选择思路:按需求复杂度决定工具
如果你在写项目代码时,问“到底该选哪个”,我更推荐按下面这个顺序判断。
优先用 synchronized 的情况
- 只是保护一个简单临界区
- 代码块不复杂
- 不需要超时、中断、公平性等高级控制
- 更希望代码短、可读、少出错
更适合用 ReentrantLock 的情况
- 需要
tryLock - 需要超时获取锁
- 需要可中断获取锁
- 需要多个
Condition - 你明确知道自己需要更细的锁控制能力
这个判断比单纯背“谁性能更好”更有价值。
十、别掉进“谁性能更好”的老问题里
synchronized 和 ReentrantLock 一直有一个经典争论:到底谁性能更好。
现实里,这个问题通常没有你想象中那么重要。
为什么
因为项目里的锁问题,真正关键的通常不是:
- API 本身快 5% 还是 10%
而是:
- 锁竞争是否严重
- 临界区是不是太大
- 你是不是拿着锁做了太多事
- 有没有设计上更好的办法减少共享状态
换句话说
如果锁竞争已经很严重,你光把 synchronized 改成 ReentrantLock,往往不会从根上解决问题。
真正更该先问的是:
- 为什么这里要锁
- 锁保护的范围是不是太大
- 能不能拆细、降级、异步或改模型
十一、一个典型选择例子:什么时候用哪个更顺
场景 1:保护一个简单计数器更新逻辑
如果只是:
- 修改共享状态
- 临界区很小
- 没有复杂等待和取消要求
那 synchronized 通常就够了,而且更不容易写错。
场景 2:任务调度中抢执行权
如果你希望:
- 线程尝试拿锁
- 拿不到就直接跳过或稍后重试
那 ReentrantLock.tryLock() 就非常合适。
场景 3:生产者/消费者模型里有多个等待条件
这类更适合 ReentrantLock + Condition,代码通常比 wait/notify 更可控。
场景 4:普通业务方法里保护成员变量
大多数时候,synchronized 已经很够用。
这几个例子能说明一个核心事实:
工程上选锁,不是选“更高级”的,而是选“刚好满足当前控制需求”的。
十二、几个非常常见的误区
1. 觉得 ReentrantLock 更高级,所以应该优先用
能力更强不代表默认更适合。多数简单场景里,synchronized 反而更自然。
2. 觉得 synchronized 已经过时
这也是误解。现代 JVM 对它的支持和优化早就不是“只能凑合用”的级别了。
3. 用 ReentrantLock 却忘了在 finally 里 unlock()
这是最经典、也最危险的使用错误之一。
4. 把注意力放在“谁快一点”,却忽略真正的锁竞争和设计问题
很多性能问题根本不在锁实现本身。
5. 需要简单互斥,却引入了过多复杂控制能力
这会让代码更难读,也更容易出错。
十三、最后留一份够用的选择 checklist
以后再遇到该选哪种锁,可以沿着这个顺序想:
第 1 步:看需求是不是简单互斥
- 如果只是保护临界区,优先考虑
synchronized
第 2 步:再看是否需要额外能力
tryLock- 超时
- 中断响应
- 多条件队列
如果需要,再考虑 ReentrantLock。
第 3 步:最后再看代码复杂度和可维护性
- 能简单解决,就别把锁模型搞复杂
- 代码越显式,越要承担更多正确性责任
十四、最后总结:项目里选锁,重点不是谁“更高级”,而是谁刚好满足你的控制需求
synchronized 和 ReentrantLock 的比较,最容易被带成“谁更强、谁更先进”的讨论。但真正落到项目里,更有价值的判断其实很朴素:
- 你只是需要一个简单可靠的互斥
- 还是你真的需要超时、中断、尝试获取锁、多个条件队列这类额外能力
如果只是前者,synchronized 往往已经很够用,而且更不容易用错;如果是后者,ReentrantLock 的灵活性才会真正体现价值。
所以比起背一堆区别,更值得记住的是这一句:
简单互斥优先选简单工具,只有在你明确需要更细锁控制能力时,再用 ReentrantLock。
只要这条原则清楚了,项目里大多数“该用哪个锁”的问题其实都不难判断。
FAQ:本地锁选型里最常被追问的几个问题
1. 大多数普通业务代码应该先默认哪一个?
如果只是简单互斥、临界区不复杂、没有超时和中断控制诉求,通常先从 synchronized 开始会更稳,也更不容易写错。
2. ReentrantLock 最大的独特价值是什么?
它最大的价值不是“更高级”,而是提供了 tryLock、可中断获取锁、超时等待和多个 Condition 这类更细的控制能力。
3. 公平锁是不是一定更好?
不是。公平锁解决的是等待顺序问题,但通常会带来额外调度成本;多数业务系统更在意整体吞吐和简单性,因此不应该把公平锁当成默认更优解。
4. 什么情况下别再纠结这两个锁,直接换一条排查线?
当你还没先搞清共享状态、复合操作和 ThreadLocal 这些更基础的问题,或者问题已经变成线程池拥塞、接口慢和性能退化时,就该继续切到并发基础页或故障主线页。
按问题继续延伸
如果你接下来更像下面这些问题,可以顺着这几条线继续延伸:
- 你还想先补齐并发基础和线程安全整体认知:
Java 项目里最常见的线程安全问题有哪些 - 你主要在处理 ThreadLocal 和线程复用边界:
ThreadLocal 常见坑有哪些?项目里最容易踩的几个问题 - 你已经进入多节点协调,需要分布式锁判断:
分布式锁什么时候该用,什么时候不该用 - 你看到的问题已经表现成线程池堆积或接口变慢:
线程池打满以后,应该先查队列、拒绝策略还是慢任务?
把它放回并发、线程池和线程安全这一条线里理解,会比孤立地把它当成 API 对比表来背更有用。
我自己会顺手连着看这几篇
如果问题已经从本地锁扩到别的层
如果你正在线上继续往下查
如果问题已经从本地锁往外扩,可以按这个顺序切到下一条线,避免一上来就把方案抬重:
- 把这篇里的 简单互斥 / tryLock / 超时 / 可中断 / Condition 几个分界点看清,再决定要不要上更复杂的锁。
- 如果你还在定位共享状态和并发 bug,去看 Java 项目里最常见的线程安全问题有哪些 和 ThreadLocal 常见坑有哪些?项目里最容易踩的几个问题。
- 如果问题已经从本地锁扩展到多节点协调,再接 分布式锁什么时候该用,什么时候不该用。
- 如果锁竞争已经表现成接口变慢、线程堆积或事务边界问题,再把 线程池打满以后,应该先查队列、拒绝策略还是慢任务?、接口响应慢怎么排查?后端性能问题定位步骤 和 Spring 事务为什么会失效?常见场景汇总 连起来看。