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/
面试考察点
- 考察对
synchronized关键字底层实现原理的理解深度,而不仅仅是知道它是用来加锁的。 - 考察是否了解 Java 对象头(Object Header)和 Monitor 机制,这是理解
synchronized实现的基础。 - 考察是否熟悉锁的优化过程(偏向锁、轻量级锁、重量级锁)以及它们在 JVM 中的演进策略,这反映了对 JVM 性能调优的理解。
- 考察能否从字节码指令层面分析
synchronized的同步块和同步方法的区别。 - 通过追问可以了解面试者是否阅读过 HotSpot 源码或深入理解过并发编程的底层细节。
核心答案
JVM 对 synchronized 的实现并不是一成不变的,它基于对象的 Monitor 机制,并且为了性能做了大量的优化。简单来说,synchronized 在 JVM 中是通过对象头(Mark Word)存储锁状态,配合Monitor 对象来实现的。从 JDK 6 开始,锁有四种状态:无锁、偏向锁、轻量级锁、重量级锁,会根据竞争情况从低到高升级,这个过程是不可逆的(即只能升级不能降级)。
深度解析
一、实现基础:对象头与 Monitor
任何一个对象都可以作为锁,这个锁的信息就存储在对象的对象头中。在 64 位 JVM 中,对象头主要由 Mark Word 和 Klass 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队列阻塞,线程切换由操作系统管理,开销较大。 - 特点:重量级锁会导致用户态与内核态的切换,因此性能较低,但能保证强互斥。
- 原理:当轻量级锁 CAS 失败且自旋达到一定次数后,锁就会膨胀为重量级锁。此时对象头 Mark Word 中存储指向 Monitor 对象的指针,线程会进入 Monitor 的
三、字节码层面的体现
synchronized 在方法级别和代码块级别的实现略有不同:
- 同步代码块:通过字节码指令
monitorenter和monitorexit来实现,一个enter对应多个exit(正常退出和异常退出),保证了锁的释放。 - 同步方法:在方法常量池中设置
ACC_SYNCHRONIZED标志,JVM 根据该标志来判断是否需要获取 Monitor。
四、其他优化
除了锁升级,JVM 还引入了锁粗化(Lock Coarsening)和锁消除(Lock Elimination)。
- 锁粗化:如果 JVM 检测到一系列连续的加锁解锁操作都是针对同一个对象,就会把这些操作扩展(粗化)成一个更大范围的锁,减少锁操作的次数。
- 锁消除:通过逃逸分析,如果 JVM 发现某个锁对象只被一个线程访问,不存在竞争,则会直接消除这个锁,提升性能。
常见误区
- 认为
synchronized一开始就是重量级锁,不了解锁优化过程。 - 混淆偏向锁、轻量级锁、重量级锁的存储结构和切换条件。
- 误以为锁升级后可以降级,实际上 HotSpot 中锁只能升级,不能降级(垃圾回收时可能短暂降级,但很快恢复)。
总结
JVM 对 synchronized 的实现是一个基于对象头的 Monitor 机制,并通过偏向锁、轻量级锁、重量级锁的渐进升级过程,以及锁粗化、锁消除等编译优化,来平衡并发性能与线程安全性。理解这些底层细节,能帮助我们写出更高效的并发程序,并在遇到性能问题时快速定位瓶颈。