直接说结论:JVM里的锁,是Java并发编程中最容易“以为自己懂了,一追问就露馅”的知识点。很多人背熟了“偏向锁→轻量级锁→重量级锁”这条升级路径,但一问到“偏向锁凭什么敢偏向第一个线程”“轻量级锁怎么 CAS 对象头”“重量级锁的 ObjectMonitor 到底在等什么”,就答不上来了。这篇文章我从底层数据结构一路往上讲,把 synchronized 的完整锁升级链路、JIT 对锁的自动优化、JUC 里各种 Lock 的实现差异,以及从 JVM 内的锁过渡到跨进程的分布式锁这整条线索串起来。不绕弯子,直接把锁的本质拆开给你看。
1. 锁的底层承载:对象头和 Mark Word 才是真正的战场
要理解 JVM 的锁,第一件事是把“锁”这个概念从抽象拉回具体。Java 中每一把锁都不是凭空存在的,synchronized 锁在对象上,而对象在 JVM 堆内存里的布局,才是锁状态的真正物理载体。
1.1 对象头里的 Mark Word 究竟存了什么
一个 Java 对象在堆内存中由三部分组成:对象头(Object Header)、实例数据(Instance Data)、对齐填充(Padding)。对象头又分为两部分:Mark Word 和 Klass Pointer(类型指针)。在 64 位 JVM 且开启指针压缩的默认配置下,Mark Word 占 8 字节,Klass Pointer 占 4 字节。
这 8 字节的 Mark Word,是整个锁机制的核心。它的存储内容会随着锁状态变化而动态复用,同一块内存,在不同时刻承载着完全不同的含义。下面这张表是把 Mark Word 的位布局摊开后的样子:
| 锁状态 | 64 位 Mark Word 存储内容(关键位) |
|---|---|
| 无锁 | 对象的 hashCode(25 位)、分代年龄(4 位)、偏向标志位(1 位,0)、锁标志位(2 位,01) |
| 偏向锁 | 持有偏向锁的线程 ID(54 位)、epoch(2 位)、分代年龄(4 位)、偏向标志位(1 位,1)、锁标志位(2 位,01) |
| 轻量级锁 | 指向栈中锁记录的指针(62 位)、锁标志位(2 位,00) |
| 重量级锁 | 指向 ObjectMonitor 的指针(62 位)、锁标志位(2 位,10) |
| GC 标记 | 空(用于 GC 标记)、锁标志位(2 位,11) |
这里有个细节值得注意:无锁状态下,Mark Word 存的是 hashCode,而且是调用 System.identityHashCode() 时生成并写入的。这意味着一个很隐蔽的结论——一旦对象调用了 identityHashCode,它就无法再进入偏向锁状态,因为偏向锁需要借用 Mark Word 里的位来存线程 ID。这是很多人踩过却没察觉的坑。
1.2 为什么锁状态能共用同一块内存
这背后的设计思路是典型的空间换时间。JVM 设计者把对象头压缩到极致,8 字节内既要有 hashCode 又要表达锁状态,只能通过位标志位来区分当前这 8 字节的“解释模式”。锁标志位(lock bits)从 01 到 00 到 10 到 11,本质上就是在切换“如何解读这 8 字节”的协议。
可以用一个生活化的类比:Mark Word 就像一块带标签的黑板。黑板还是那块黑板,但黑板左上角贴的标签不同,上面写的字含义就不同。标签写着“偏向锁”,黑板内容就是线程 ID;标签写着“轻量级锁”,黑板内容就成了指向栈帧的指针。
理解了这层复用关系,后续看锁升级,本质就是在追问:同一块 Mark Word,在什么条件下被改写成什么内容。所有的 CAS 操作、所有的状态流转,目标都是这 8 字节。
1.3 实操:用 JOL 亲眼看一下对象头
空谈理论不如自己验证。OpenJDK 提供的 jol(Java Object Layout)工具可以直接打印对象内存布局,我在分析锁问题时几乎每次都先用它确认当前 JVM 的配置和对象实际布局。引入依赖后,一个 main 方法就能看:
java复制public class ObjectLayoutDemo {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
输出中会清晰显示 Mark Word 的 8 字节十六进制值。注意,在默认配置下你看到的偏向锁可能是延迟开启的——HotSpot 通过 -XX:BiasedLockingStartupDelay 参数默认延迟 4 秒才启动偏向锁,这是为了避免 JVM 启动阶段大量无意义的偏向锁撤销。想立刻看到偏向锁效果,可以启动时加 -XX:BiasedLockingStartupDelay=0。
我用 JOL 验证过一次印
