Java

synchronized 和 ReentrantLock 有什么区别?项目里该怎么选

`synchronized` 和 `ReentrantLock` 的区别不难背,难的是放回项目代码里时到底该为哪些能力多付复杂度。把简单互斥、超时、中断、Condition 这些真实需求拆开,选型会比直接背特点更靠谱。

  • Java
  • 并发
  • synchronized
  • ReentrantLock
14 分钟阅读

synchronizedReentrantLock 是 Java 并发里最常被放在一起比较的一组工具。只要聊到线程安全、锁竞争、并发控制,几乎都会有人问:它们到底有什么区别,项目里该用哪个。很多人也知道一些常见答案,比如:

  • ReentrantLock 更灵活
  • synchronized 写法更简单
  • ReentrantLock 可以中断、可以超时
  • synchronized 是 JVM 关键字

这些说法都不算错,但如果只是停留在这个层面,到了真实项目里还是不一定知道怎么选。因为工程上真正有价值的问题不是“背出它们的特点”,而是:

当前这个并发问题,到底只是要一个简单可靠的互斥,还是需要更复杂的锁控制能力?

我想讲清的不是面试题答案,而是项目里到底要不要为 tryLock、超时、中断、Condition 这些能力多承担一层心智负担。很多场景里,锁的能力越多不等于越划算。

这篇讨论的是本地锁选型

先用这张表把范围收住,别把本地锁选择和别的并发问题揉在一起。

你现在看到的现象更像什么问题更值得先看什么
你在写并发控制代码,想判断 synchronized 还是 ReentrantLock 更合适本地锁选型直接往下看这篇
你更想梳理项目里常见的线程安全坑,而不只是一把锁并发基础问题去看 Java 项目里最常见的线程安全问题有哪些
你主要在处理 ThreadLocal 串用、线程复用和上下文边界ThreadLocal 边界问题ThreadLocal 常见坑有哪些?项目里最容易踩的几个问题
你已经进入分布式协调,多节点竞争是核心问题分布式锁选型分布式锁什么时候该用,什么时候不该用
现象已经表现为线程池堆积、接口变慢或执行链拥塞故障排查主线线程池打满以后,应该先查队列、拒绝策略还是慢任务?

讨论范围:JVM 内的本地互斥

这里讨论的是 JVM 进程内的本地锁怎么选。

如果你手上的问题其实是:

  • 共享状态和复合操作到底哪里不安全
  • ThreadLocal 在线程复用下怎么串值
  • 多节点竞争要不要上分布式锁
  • 线程池、接口 RT、CPU 已经开始一起变差

那就不该只停在这里。

读这篇更合适的前提,是你已经确认自己面对的是本地临界区互斥,现在只差把 synchronizedReentrantLock 选明白。

一、先有一个总认识:它们解决的核心问题是一样的

先别被写法和 API 形式带偏。

synchronizedReentrantLock 虽然一个是关键字,一个是类,但它们最核心的目标其实是一样的:

  • 在多线程环境下
  • 保护共享可变状态
  • 让同一时刻只有有限线程进入临界区

也就是说,它们都属于“互斥控制工具”。

所以真正的区别不在于“谁能加锁谁不能”

而在于:

  • 锁的获取方式有多灵活
  • 失败和等待时能不能做更多控制
  • 用起来的复杂度和出错成本有多高

先把这个大前提抓住,后面就不会把它们想成两种完全不同的问题解法。

二、第一层区别: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
  • 你明确知道自己需要更细的锁控制能力

这个判断比单纯背“谁性能更好”更有价值。

十、别掉进“谁性能更好”的老问题里

synchronizedReentrantLock 一直有一个经典争论:到底谁性能更好。

现实里,这个问题通常没有你想象中那么重要。

为什么

因为项目里的锁问题,真正关键的通常不是:

  • 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 却忘了在 finallyunlock()

这是最经典、也最危险的使用错误之一。

4. 把注意力放在“谁快一点”,却忽略真正的锁竞争和设计问题

很多性能问题根本不在锁实现本身。

5. 需要简单互斥,却引入了过多复杂控制能力

这会让代码更难读,也更容易出错。

十三、最后留一份够用的选择 checklist

以后再遇到该选哪种锁,可以沿着这个顺序想:

第 1 步:看需求是不是简单互斥

  • 如果只是保护临界区,优先考虑 synchronized

第 2 步:再看是否需要额外能力

  • tryLock
  • 超时
  • 中断响应
  • 多条件队列

如果需要,再考虑 ReentrantLock

第 3 步:最后再看代码复杂度和可维护性

  • 能简单解决,就别把锁模型搞复杂
  • 代码越显式,越要承担更多正确性责任

十四、最后总结:项目里选锁,重点不是谁“更高级”,而是谁刚好满足你的控制需求

synchronizedReentrantLock 的比较,最容易被带成“谁更强、谁更先进”的讨论。但真正落到项目里,更有价值的判断其实很朴素:

  • 你只是需要一个简单可靠的互斥
  • 还是你真的需要超时、中断、尝试获取锁、多个条件队列这类额外能力

如果只是前者,synchronized 往往已经很够用,而且更不容易用错;如果是后者,ReentrantLock 的灵活性才会真正体现价值。

所以比起背一堆区别,更值得记住的是这一句:

简单互斥优先选简单工具,只有在你明确需要更细锁控制能力时,再用 ReentrantLock。

只要这条原则清楚了,项目里大多数“该用哪个锁”的问题其实都不难判断。

FAQ:本地锁选型里最常被追问的几个问题

1. 大多数普通业务代码应该先默认哪一个?

如果只是简单互斥、临界区不复杂、没有超时和中断控制诉求,通常先从 synchronized 开始会更稳,也更不容易写错。

2. ReentrantLock 最大的独特价值是什么?

它最大的价值不是“更高级”,而是提供了 tryLock、可中断获取锁、超时等待和多个 Condition 这类更细的控制能力。

3. 公平锁是不是一定更好?

不是。公平锁解决的是等待顺序问题,但通常会带来额外调度成本;多数业务系统更在意整体吞吐和简单性,因此不应该把公平锁当成默认更优解。

4. 什么情况下别再纠结这两个锁,直接换一条排查线?

当你还没先搞清共享状态、复合操作和 ThreadLocal 这些更基础的问题,或者问题已经变成线程池拥塞、接口慢和性能退化时,就该继续切到并发基础页或故障主线页。

按问题继续延伸

如果你接下来更像下面这些问题,可以顺着这几条线继续延伸:

把它放回并发、线程池和线程安全这一条线里理解,会比孤立地把它当成 API 对比表来背更有用。

我自己会顺手连着看这几篇

如果问题已经从本地锁扩到别的层

如果你正在线上继续往下查

如果问题已经从本地锁往外扩,可以按这个顺序切到下一条线,避免一上来就把方案抬重:

  1. 把这篇里的 简单互斥 / tryLock / 超时 / 可中断 / Condition 几个分界点看清,再决定要不要上更复杂的锁。
  2. 如果你还在定位共享状态和并发 bug,去看 Java 项目里最常见的线程安全问题有哪些ThreadLocal 常见坑有哪些?项目里最容易踩的几个问题
  3. 如果问题已经从本地锁扩展到多节点协调,再接 分布式锁什么时候该用,什么时候不该用
  4. 如果锁竞争已经表现成接口变慢、线程堆积或事务边界问题,再把 线程池打满以后,应该先查队列、拒绝策略还是慢任务?接口响应慢怎么排查?后端性能问题定位步骤Spring 事务为什么会失效?常见场景汇总 连起来看。