如何通过分析 Synchronized 的锁膨胀机制理解从偏向锁到重量级锁的位状态迁移

简单来说,锁的状态就藏在对象头的标记位里:偏向锁的Mark Word低3位是101,轻量级锁是000,而重量级锁则是010。识别这些位模式,并理解它们之间不可逆的迁移路径,是掌握Synchronized底层机制的关键。当然,这个过程不能光靠理论推演,必须结合字节码分析、JOL工具输出和特定的JVM参数来交叉验证。
偏向锁的 Mark Word 位模式怎么识别
偏向锁的实现,绕开了操作系统的互斥量,其奥秘全在对象头那个小小的Mark Word里。具体来说,它依赖里面的线程ID和epoch字段来做标记。当一个对象尚未被任何锁定时,它的Mark Word低3位显示为001,代表无锁状态。一旦JVM启用了偏向锁优化,并且这个对象第一次被某个线程获取,JVM就会通过CAS操作,把当前线程的ID写入Mark Word的高位区域,同时把低3位从001改为101——这个101就是偏向锁独一无二的身份证。
不过,在实际观察时,有几个坑很容易踩到:
- 直接去读
Mark Word的十六进制值,却忽略了JVM在内存中的大端/小端布局,导致位解析完全错位。 - 在已经通过
-XX:-UseBiasedLocking参数全局禁用偏向锁的环境里,还试图寻找偏向锁的行为,自然是徒劳无功。 - 使用JOL(Ja va Object Layout)这个神器来查看对象布局时,如果没加上
-XX:BiasedLockingStartupDelay=0参数,那么JVM默认会有4秒的延迟,在这期间创建的对象是不会进入偏向状态的。
轻量级锁触发时 Mark Word 怎么变化
天下没有不散的筵席,偏向锁的“独占”状态也是。当另一个线程也来尝试获取这个已经被偏向的锁时,故事就进入了下一章。JVM会首先执行偏向锁撤销(revoke),然后尝试让竞争线程走轻量级锁的流程。这个过程的核心,依然是CAS:线程会试图将自己栈帧中的锁记录(Lock Record)地址写入对象的Mark Word。如果成功,Mark Word的低3位就会从101变成000,这个000正是轻量级锁的标志。此时,对象头里存储的不再是线程ID,而是一个指向线程栈空间的指针。
这里有几个关键细节值得玩味:
- CAS失败并不意味着锁会立刻膨胀升级。失败后,线程会进入自旋(默认10次,可通过
-XX:PreBlockSpin调整),在自旋期间,Mark Word依然保持着000的轻量级锁状态。 - 如果在自旋过程中,原来持有锁的线程恰好退出了同步块,锁会被释放,
Mark Word会恢复为无锁的001。这时,自旋的线程就有机会重新竞争,甚至可能再次让对象偏向自己。 - 只有当自旋耗尽,锁依然没能获取成功,才会真正触发锁膨胀。这时,
Mark Word的内容会被替换为指向一个重量级ObjectMonitor结构的指针,其低3位也随之变为010。
重量级锁的 010 标识为什么不可逆
当你在对象头里看到010这个三位标识时,就意味着对象已经进入了重量级锁的领域。此时,Mark Word里存储的是一个指向ObjectMonitor结构的指针。这个结构由JVM在堆上专门分配,并且与操作系统底层的mutex(互斥量)相关联,因此锁操作会涉及用户态到内核态的切换,开销自然就上去了。
那么,为什么说这个过程是不可逆的呢?锁一旦升级为重量级,就再也回不去了,这背后有几个根本原因:
ObjectMonitor结构一旦创建,其内部维护的队列(如_EntryList、_WaitSet)可能已经挂载了正在阻塞等待的线程。如果此时降级,这些线程的等待状态和唤醒语义将无法得到保证,会引发混乱。- 偏向锁和轻量级锁的生命周期与线程的栈帧紧密绑定。而重量级锁的
ObjectMonitor是独立于栈帧存在的堆对象,管理域完全不同,技术上无法安全地回退到依赖栈帧的轻量级状态。 - 从JVM源码的实现来看,锁膨胀(
inflate)是一个单向的构造过程,并没有设计对应的收索(deflate)逻辑来支持降级。
怎么验证锁状态迁移过程
理论说得再透彻,不如动手验证一遍。要真正看清锁状态的迁移,不能依赖猜测或片面的日志,必须进行交叉验证,主要从三个维度入手:字节码、对象头实际数据和JVM运行时参数。
- 首先,用
ja vap -v命令反编译你的Class文件,确认同步块是否确实生成了monitorenter和monitorexit指令。 - 接着,借助JOL工具,通过
ClassLayout.parseInstance(obj).toPrintable()实时打印出对象的布局,特别是Mark Word的十六进制值。然后,你需要对照JVM源码(比如markOop.hpp文件)中的位定义表,像解码密码一样解析出每一位的含义。 - 此外,可以加上
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1这样的JVM参数,观察偏向锁撤销时是否会引发全局安全点(Safepoint),这对理解性能影响很有帮助。 - 最后,注意避开一些“陷阱”对象。比如容器类(
HashMap)或字符串常量池中的对象,它们内部可能默认禁用了偏向锁,在这上面测试会得不到预期的结果。
说到底,最容易忽略的一个核心点是:锁膨胀并非发生在你调用synchronized的那一刻,而是在后续线程**竞争获取锁失败时**才被触发。而且,所有的状态变更都是以单个对象为粒度的,这意味着同一个类的两个不同实例,完全可能处于截然不同的锁状态——一个可能还是无锁,另一个可能已经膨胀为重量级锁了。理解这一点,才能算真正摸清了Synchronized的脾气。
