JVM 对 Synchronized 关键字的实现是怎样的?

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

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

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 考察对 synchronized 关键字底层实现原理的理解深度,而不仅仅是知道它是用来加锁的。
  2. 考察是否了解 Java 对象头(Object Header)和 Monitor 机制,这是理解 synchronized 实现的基础。
  3. 考察是否熟悉锁的优化过程(偏向锁、轻量级锁、重量级锁)以及它们在 JVM 中的演进策略,这反映了对 JVM 性能调优的理解。
  4. 考察能否从字节码指令层面分析 synchronized 的同步块和同步方法的区别。
  5. 通过追问可以了解面试者是否阅读过 HotSpot 源码或深入理解过并发编程的底层细节。

核心答案

JVM 对 synchronized 的实现并不是一成不变的,它基于对象的 Monitor 机制,并且为了性能做了大量的优化。简单来说,synchronized 在 JVM 中是通过对象头(Mark Word)存储锁状态,配合Monitor 对象来实现的。从 JDK 6 开始,锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁,会根据竞争情况从低到高升级,这个过程是不可逆的(即只能升级不能降级)。

深度解析

一、实现基础:对象头与 Monitor

任何一个对象都可以作为锁,这个锁的信息就存储在对象的对象头中。在 64 位 JVM 中,对象头主要由 Mark WordKlass Pointer 组成,其中 Mark Word 在默认情况下存储对象的 HashCode、分代年龄以及锁标志位等信息。当对象被用作锁时,Mark Word 的内容会根据锁状态发生变化。

每个 Java 对象都与一个 Monitor 对象关联(在 HotSpot 源码中是 ObjectMonitor),Monitor 中记录了当前持有锁的线程、等待队列等信息。当多个线程同时请求一个对象的锁时,JVM 会通过 Monitor 来协调线程的竞争。

二、锁的升级过程

为了减少锁竞争带来的上下文切换开销,JVM 引入了锁升级机制,整个过程偏向于“乐观”:

  • 偏向锁(Biased Locking)

    • 原理:当一个线程第一次获得锁时,JVM 会将对象头 Mark Word 中的线程 ID 设置为当前线程,并设置偏向锁标志位。此后该线程再次进入同步块时,无需任何同步操作(CAS),直接执行,性能极高。
    • 适用场景:锁不仅不存在多线程竞争,而且总是由同一个线程多次获取。这在很多单线程访问共享资源的场景下非常适用。
    • 撤销:如果有另一个线程尝试竞争偏向锁,偏向锁会被撤销,升级为轻量级锁。撤销过程需要在全局安全点执行,有一定开销。
  • 轻量级锁(Lightweight Locking)

    • 原理:当竞争出现时,偏向锁会升级为轻量级锁。此时线程会在自己的栈帧中创建一块空间(Lock Record)用于存储锁对象的 Mark Word 副本。然后线程尝试通过 CAS 操作将对象头的 Mark Word 替换为指向 Lock Record 的指针。如果成功,则获取锁;如果失败,说明有竞争,锁会膨胀为重量级锁。
    • 适用场景:线程交替执行同步块,不存在长时间竞争。CAS 操作避免了线程阻塞,性能较好。
    • 自旋:在轻量级锁阶段,JVM 会允许线程进行几次自旋(空循环)尝试获取锁,而不是立即挂起线程,这进一步减少了线程阻塞的开销。
  • 重量级锁(Heavyweight Locking)

    • 原理:当轻量级锁 CAS 失败且自旋达到一定次数后,锁就会膨胀为重量级锁。此时对象头 Mark Word 中存储指向 Monitor 对象的指针,线程会进入 Monitor 的 _EntryList 队列阻塞,线程切换由操作系统管理,开销较大。
    • 特点:重量级锁会导致用户态与内核态的切换,因此性能较低,但能保证强互斥。

三、字节码层面的体现

synchronized 在方法级别和代码块级别的实现略有不同:

  • 同步代码块:通过字节码指令 monitorentermonitorexit 来实现,一个 enter 对应多个 exit(正常退出和异常退出),保证了锁的释放。
  • 同步方法:在方法常量池中设置 ACC_SYNCHRONIZED 标志,JVM 根据该标志来判断是否需要获取 Monitor。

四、其他优化

除了锁升级,JVM 还引入了锁粗化(Lock Coarsening)锁消除(Lock Elimination)

  • 锁粗化:如果 JVM 检测到一系列连续的加锁解锁操作都是针对同一个对象,就会把这些操作扩展(粗化)成一个更大范围的锁,减少锁操作的次数。
  • 锁消除:通过逃逸分析,如果 JVM 发现某个锁对象只被一个线程访问,不存在竞争,则会直接消除这个锁,提升性能。

常见误区

  • 认为 synchronized 一开始就是重量级锁,不了解锁优化过程。
  • 混淆偏向锁、轻量级锁、重量级锁的存储结构和切换条件。
  • 误以为锁升级后可以降级,实际上 HotSpot 中锁只能升级,不能降级(垃圾回收时可能短暂降级,但很快恢复)。

总结

JVM 对 synchronized 的实现是一个基于对象头的 Monitor 机制,并通过偏向锁、轻量级锁、重量级锁的渐进升级过程,以及锁粗化、锁消除等编译优化,来平衡并发性能与线程安全性。理解这些底层细节,能帮助我们写出更高效的并发程序,并在遇到性能问题时快速定位瓶颈。