Skip to content
Java 锁实现与并发细节 · 第 1 篇 / 共 4 篇
领域编程与框架
专题Java 专题
当前序列Java 锁实现与并发细节
阅读位置第 1 篇 / 共 4 篇当前专题第 6 个序列 / 共 10 个序列

synchronized 为什么仍然是 Java 并发里的默认首选 ​

synchronized 这几年经常被初学者误解成“老旧、重量级、性能差”的方案,于是很多人一上来就想用 Lock、CAS、无锁容器把代码写得很“高级”。

但在真实业务里,synchronized 依然是最稳、最省心、综合成本最低的默认互斥方案之一。它的问题往往不在锁本身,而在我们没有先分清场景。

先说结论 ​

  • 普通对象级互斥、短临界区、可读性优先的业务代码,优先用 synchronized
  • synchronized 不是一直都“重量级”,JVM 会根据竞争情况做偏向、轻量级、自旋到重量级的升级
  • 真正该重点优化的通常不是“换锁”,而是缩短临界区、减少共享状态、避免锁内 IO
  • 只有当你需要可中断、可超时、条件队列、公平锁等能力时,才优先考虑 ReentrantLock

它到底解决什么问题 ​

synchronized 解决的是多线程访问共享状态时的互斥和可见性问题:

  • 同一时刻只允许一个线程进入临界区
  • 退出同步块时,把工作内存中的修改刷新到主内存
  • 进入同步块时,读到的是加锁释放后对共享变量的最新结果

对业务代码来说,这已经覆盖了大量需求,比如:

  • 库存扣减前的本地内存状态保护
  • 单机内对某个 Map、List、计数器的互斥更新
  • 订单状态机在 JVM 内的串行推进

为什么它仍然是默认首选 ​

1. 语义最直接 ​

synchronized 是 Java 语言层面的能力,不需要开发者额外维护加锁和解锁配对。

java
public synchronized void addItem(Item item) {
    items.add(item);
    total++;
}

相比显式锁,它天然少一个风险点: 忘记 unlock()。

2. 异常安全 ​

同步块退出时,JVM 会自动释放锁。
这意味着它对业务代码更加宽容,尤其适合新团队和高频维护代码。

3. JVM 已经替你做了很多优化 ​

现代 JVM 并不是一上来就走重量级互斥。一般会经历这样一条路径:

  • 无竞争: 直接进入
  • 轻度竞争: CAS + 自旋
  • 竞争加剧: 膨胀成重量级监视器锁

所以很多“synchronized 性能差”的说法,放到现在已经不准确了。

锁升级到底在升级什么 ​

可以把它理解为 JVM 在动态选择更合适的同步成本。

1. 无锁或偏向状态 ​

没有竞争时,对象头里会记录偏向线程信息。
适合同一线程反复进入同一个同步块的情况。

2. 轻量级锁 ​

多个线程偶尔竞争时,JVM 会优先尝试 CAS 抢占锁记录。
这时线程不一定立刻阻塞,往往先自旋等待。

3. 重量级锁 ​

竞争激烈、自旋无效时,锁会膨胀到重量级监视器,线程进入阻塞和唤醒流程。
这时会涉及用户态和内核态切换,成本就上来了。

重点不是记这几个名词,而是知道:

  • 低竞争下,synchronized 完全可以很轻
  • 真正的性能问题通常来自高竞争,而不是关键字本身

什么时候它特别合适 ​

下面这些场景,synchronized 往往是非常好的第一选择:

  • 方法级或对象级的简单互斥
  • 临界区很短,只做内存计算,不做外部调用
  • 团队更看重代码稳定性和可维护性
  • 并发量不算夸张,或者热点竞争点不多

典型例子:

java
public void submit(OrderCommand command) {
    synchronized (this) {
        if (closed) {
            throw new IllegalStateException("queue closed");
        }
        queue.add(command);
    }
}

这类代码没有必要为了“高级感”改成复杂的 CAS 结构。

什么时候不该死用 synchronized ​

1. 需要可中断锁等待 ​

比如线程等待期间需要响应取消,就该考虑 lockInterruptibly()。

2. 需要超时获取锁 ​

比如抢锁失败要快速降级,而不是无限等待,tryLock() 更合适。

3. 需要多个条件队列 ​

生产者消费者模型里,一个锁下可能需要多个条件队列,这时 Condition 更灵活。

4. 锁内存在慢操作 ​

如果锁内有数据库、RPC、文件 IO,再好的锁都会出问题。
这里该优化的是临界区边界,而不是锁类型。

工程里真正的高频坑 ​

1. 锁范围过大 ​

把查询、拼装、远程调用都放进同步块,锁竞争一定会迅速恶化。

2. 锁对象选错 ​

比如用字符串常量、自动装箱对象做锁,容易出现意外共享。

3. 共享状态设计过粗 ​

很多人觉得是锁慢,实际上是一个全局对象承载了太多互斥职责。

更好的思路通常是:

  • 按用户、订单、分片拆分锁粒度
  • 尽量减少可变共享状态
  • 读多写少场景改成更合适的数据结构

面试和生产都能用的一句话判断 ​

如果你面对的是 JVM 内简单互斥问题,先问自己三件事:

  1. 临界区是否足够短
  2. 是否需要超时、中断、条件队列等额外能力
  3. 是否真的存在高竞争

如果前两项答案都是否,第三项又不严重,那 synchronized 大概率就是最合适的默认方案。

总结 ​

synchronized 到今天依然重要,不是因为它“传统”,而是因为它在大量普通业务场景里依然是成本最低、语义最清晰、最不容易写错的同步方式。

真正值得优化的方向,不是盲目把它替换掉,而是把锁粒度、共享状态和临界区边界设计清楚。

延伸阅读相关文章优先当前专题,再补跨专题关联。
同一序列 · 顺着当前主线继续读CAS 的 ABA 问题怎么理解适合把 synchronized、CAS、LongAdder 和并发容器的适用边界放到一起看。Java 专题 · Java 锁实现与并发细节同一序列 · 顺着当前主线继续读AtomicLong 和 LongAdder 怎么选适合把 synchronized、CAS、LongAdder 和并发容器的适用边界放到一起看。Java 专题 · Java 锁实现与并发细节同专题其他序列 · 共享标签:并发线程池参数怎么配适合把线程池参数、拒绝策略、CompletableFuture 和 ForkJoin 放在一起连续看。Java 专题 · Java 线程池与异步编排同专题其他序列 · 共享标签:并发false sharing 为什么会拖慢并发程序适合把 happens-before、volatile、原子类和阶段式同步器放在一条并发语义主线上理解。Java 专题 · Java 并发语义与同步器细节跨专题关联 · 同场景:基础学习autocommit 为什么也会影响锁行为适合把 next-key lock、一致性读、复制延迟和分库分表边界放回同一条一致性主线上判断。MySQL 专题 · MySQL 事务、锁与高可用边界跨专题关联 · 同场景:基础学习@Conditional 系列注解怎么配合使用适合把自动装配、条件装配、Profile 和循环依赖放回 Spring Boot 启动过程里理解。Spring 专题 · Spring Boot 启动、装配与配置
继续阅读Java 锁实现与并发细节当前序列第 1 篇 / 共 4 篇当前专题第 6 个序列 / 共 10 个序列
往前看
上一序列JVM 与进程排障从第 1 篇开始:JVM 内存问题怎么分析
往后看
下一篇CAS 的 ABA 问题怎么理解继续当前序列下一章下一序列Java 类加载与运行时细节从第 1 篇开始:双亲委派模型到底解决了什么

把零散经验整理成可查、可复用、可持续更新的企业级知识门户