synchronized 锁升级过程是怎样的?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 锁优化机制理解:面试官不仅仅是想知道 "锁会升级" 这个结论,更是想知道你是否理解 JDK 6 引入锁升级的动机——从 "一切皆重量级锁" 到 "按竞争程度分级优化" 的设计思路。

  2. 对象头与 Mark Word:考察你是否了解 synchronized 的底层存储结构,能否说清楚锁状态存储在对象头的 Mark Word 中,以及不同锁状态下 Mark Word 存储的内容差异。

  3. 锁升级方向性:考察你是否清楚锁升级是 单向的(只升不降),以及为什么这样设计。这个点很多候选人会搞错,以为锁用完了还会降级回去。

核心答案

先甩结论:JDK 6 开始,synchronized 引入了锁升级机制,锁的状态从低到高依次为:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。锁只能升级,不能降级。

锁状态 适用场景 获取锁方式 性能开销
无锁 没有线程访问同步代码
偏向锁 只有一个线程反复进入同步块 CAS 比较 Thread ID,无需同步 极低
轻量级锁 两个线程交替执行同步块 CAS 自旋尝试 较低
重量级锁 多线程激烈竞争 操作系统互斥量(Mutex

核心思路就一句话:能不惊动操作系统就不惊动,能轻量解决就别上重量级

深度解析

一、对象头与 Mark Word

要理解锁升级,先得搞清楚锁状态存在哪。JVM 中每个对象都有一个 "对象头"(Object Header),其中 Mark Word 区域就是锁状态的 "指挥部"。

上面这个表格展示了不同锁状态下 Mark Word 存储的不同内容。几个关键点:

  • 锁标志位(2 bit)占 4 种状态,其中 01 还需要一个 偏向标志位(1 bit)来区分是 "无锁" 还是 "偏向锁"
  • 偏向锁状态下,Mark Word 里记录的是 线程 ID,这比 CAS 还轻量——直接比较 Thread ID 就行
  • 轻量级锁和重量级锁存的都是 指针,一个指向栈帧中的锁记录,一个指向 ObjectMonitor

这块确实绕,我当年也理解了好几遍。别急,看完下面的升级流程就通透了。

二、锁升级完整流程

上图展示了锁从无锁到重量级锁的完整升级链路。下面逐个阶段拆解:

阶段一:无锁 → 偏向锁

  • 当一个线程第一次进入同步块时,JVM 通过 CAS 将该线程的 ID 写入 Mark Word
  • 之后该线程再次进入,只需比较 Thread ID 是否一致,一致就直接进入,连 CAS 都不用
  • 这就是 "偏向" 的含义——锁偏向于第一个获取它的线程

阶段二:偏向锁 → 轻量级锁

  • 第二个线程 尝试获取这个锁时,偏向锁被撤销
  • JVM 会先暂停拥有偏向锁的线程(STW),检查持有偏向锁的线程是否还活着、是否还在同步块中
  • 如果原线程已退出同步块,可以直接撤销偏向锁,新线程通过 CAS 竞争轻量级锁
  • 如果原线程还在同步块内,则升级为轻量级锁,双方通过 CAS 自旋 竞争

阶段三:轻量级锁 → 重量级锁

  • 轻量级锁通过 CAS 自旋获取,但如果自旋超过一定次数(自适应自旋),或者又有更多线程来竞争
  • JVM 会将锁升级为重量级锁,调用操作系统的 Mutex Lock
  • 此时未获取到锁的线程会被 阻塞挂起park),涉及用户态到内核态的切换,开销较大

三、为什么锁只升不降?

这个设计决策很有讲究。原因很简单:

  • 场景驱动:如果锁曾经被激烈竞争过,大概率后续还会被激烈竞争,降级没有实际收益
  • 避免抖动:如果允许升降级,锁状态会在边界条件下来回切换,这个 "抖动" 本身就带来额外开销
  • 实现简洁:单向升级的实现复杂度远低于双向变化,JVM 团队选择了工程上更稳健的方案

四、偏向锁的批量重偏向与批量撤销

这块属于加分项。JVM 对偏向锁还有两层优化:

  • 批量重偏向:当一个类的对象频繁发生偏向锁撤销时(超过阈值,默认 20 次),JVM 会批量将这些对象重新偏向到当前线程,避免反复撤销
  • 批量撤销:如果撤销次数继续飙升(默认 40 次),JVM 直接关闭该类的偏向锁,之后该类的新对象直接从轻量级锁开始

五、JDK 15 废弃偏向锁

如果你面试的是 JDK 15+ 的场景,面试官可能会追问这个点:

# JDK 15 默认关闭偏向锁
-XX:-UseBiasedLocking   # 手动关闭(JDK 15 前可用)

为什么废弃?因为现代 JVM 的自旋锁优化已经很好了,而且偏向锁的撤销需要 STW,在多核高并发场景下反而成了瓶颈。简单说,偏向锁的收益不值得它带来的复杂性了

面试高频追问

  1. 偏向锁撤销的代价高吗?

    高。偏向锁撤销需要等待全局安全点(Safepoint),也就是需要 STW。如果应用中频繁出现偏向锁撤销,会直接影响 GC 和性能。这也是批量重偏向和批量撤销机制存在的原因。

  2. 自适应自旋是什么?

    JDK 6 引入了自适应自旋。JVM 会根据 上一次自旋是否成功 来动态调整下一次自旋的次数。如果上次自旋成功了,下次可能会增加自旋次数;如果上次很少成功,下次可能直接跳过自旋进入阻塞。说白了就是 JVM 帮你 "学习" 最优策略。

  3. synchronizedReentrantLock 在锁机制上的区别?

    synchronized 基于 JVM 层面的 Monitor,支持锁升级;ReentrantLock 基于 AQS,通过 CAS + CLH 队列实现,支持公平锁、可中断、多条件变量等高级特性。两者没有绝对的优劣,看场景选。

常见面试变体

  • "synchronized 锁存在对象头的哪个位置?"
  • "偏向锁和轻量级锁的区别是什么?"
  • "什么情况下 synchronized 会膨胀为重量级锁?"
  • "为什么 JDK 15 要废弃偏向锁?"

记忆口诀

升级路径:无锁 → 偏向 → 轻量 → 重量(记:"无偏轻重,只升不降"

各阶段核心操作

  • 偏向锁:比 Thread ID,不用 CAS
  • 轻量级锁:CAS 自旋,不阻塞
  • 重量级锁:OS Mutex,线程阻塞

总结

synchronized 锁升级是 JVM 对 synchronized 性能优化的核心体现。从无锁到偏向锁到轻量级锁再到重量级锁,本质上是在 用最小的代价解决不同竞争程度的问题。记住 "无偏轻重,只升不降" 这条主线,再把对象头 Mark Word 的结构和各阶段触发条件搞清楚,这道题基本能拿满分。