前阵子帮朋友看一个线上问题,服务在高峰期 CPU 直接飙到满,线程 Dump 拉下来一看,一大片线程卡在同一个业务对象的锁上,状态全是 BLOCKED。当时第一反应是“是不是锁粒度太大了”,但顺着字节码和 JVM 日志一路查下去才发现,问题远不止“加锁位置不对”这么简单——synchronized 在现代 JVM 里早就不是我们以为的“又慢又重的老古董”了,它从偏向锁、轻量级锁到重量级锁有一条完整的升级链路,每个阶段的行为、触发条件、性能损耗都不一样。今天这篇文章,我就从 JVM 视角把 synchronized 的底层实现梳理一遍,把对象头、Monitor、锁升级、以及 JMM 带来的内存语义这些硬骨头啃下来。无论你是准备面试还是排查线上锁竞争,这份笔记都值得收藏。
1. synchronized 到底在 JVM 里管了什么
1.1 先搞清楚它解决的是什么问题
Synchronized 并不是一个简单的“某个变量上的标志位”,它是 JVM 通过互斥机制实现线程安全的基础设施。从最朴素的视角看,它解决的是“多线程同时访问一块共享数据”的竞争问题。多线程并发程序里最大的麻烦在于,两个线程同时读写同一个变量,你没法预测最终结果是哪个线程的写入生效。比如账务系统里一个简单的余额变更,如果不用锁保护,就会出现“读到一个旧值、加 100、再写回去”的覆盖场景,这笔账就对了不上。
早年我们学 Java 并发时,老师会告诉你 synchronized 可以保证原子性、可见性、有序性。原子性指的是临界区代码要么全部执行完,要么不执行,线程切换不能把一段临界区拆成两半;可见性指的是一个线程修改了共享变量,另一个线程能立刻读到最新值,而不是读到 CPU 缓存里的旧值;有序性指的是 JVM 和 CPU 在指令重排时不能随意越过锁的边界。这三条,其实都是 JVM 在实现 synchronized 时强制附加的内存语义,并不是语法层面天然保证的。理解了这三条,你才能理解为什么锁的“底层实现”不是一句“加个锁”就能概括的。
这里我习惯用一个生活例子帮助理解:把临界区想象成公司里只有一间的会议室,线程就是来开会的人。synchronized 做的就是“一个人进去时把门锁上,其他人只能在门口排队”。但问题在于,JVM 为了性能,设计了三种“门锁”:如果基本没人抢,就给来开会的人一个专属门禁卡,每次刷卡直接就进;如果有人插队了,就在门口快速抢一下门把手,抢不到再转圈等;如果外面排队的人越来越多,就干脆请一个真正的保安(操作系统)来排队叫号。这个类比基本就对应了偏向锁、轻量级锁和重量级锁。
1.2 从全局看它在 JVM 里的定位
从 JVM 内部实现的角度来看,synchronized 并不仅是一个关键字那么简单,它对应着一整套的运行时协作机制,涉及字节码指令、对象内存布局(特别是对象头 Mark Word)、VM 内部的 ObjectMonitor 数据结构,以及操作系统提供的互斥量(mutex)和条件变量(condition variable)。当你在 Java 源码里写下 synchronized 时,javac 把它编译成字节码;JVM 的 Interpreter 和 C1/C2 编译器在解释执行或即时编译时,再把这些字节码转换成对应的运行时逻辑。
我们需要知道的关键边界是:synchronized 的语义由 JVM 规范定义,但具体实现是 HotSpot 源码里的东西。规范层面只规定了 monitorenter/monitorexit 指令和 ACC_SYNCHRONIZED 标志的行为,但并没有规定必须用偏向锁、轻量级锁这些机制。这些机制是 HotSpot 团队在 JDK 6 以后为性能做的优化,是“实现细节”而非“规范要求”。所以你在读不同版本的 JDK、不同厂商的 JVM 时,锁的具体表现可能会有差异,这也是排查线上问题时要特别注意的地方。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把字节码摆出来看
2.1 同步代码块与同步方法的字节码差异
要理解 synchronized 的底层实现,第一步不是直接钻源码,而是先用 javap 看一眼它的字节码长什么样。我们来写一段最简单的代码:
java复制public class SyncDemo {
private final Object lock = new Object();
public void blockMethod() {
synchronized (lock) {
System.out.println("hello");
}
}
public synchronized void syncMethod() {
System.out.println("world");
}
}
用 javap -v SyncDemo.class 反编译后,blockMethod() 的字节码大致是:
text复制public void blockMethod();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: getstatic #7
7: ldc #13
9: invokevirtual #15
12: aload_2
13: monitorexit
14: goto 22
17: astore_3
18: aload_2
19: monitorexit
20: aload_3
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
而 syncMethod() 的字节码里根本没有 monitorenter 和 monitorexit,它只是在方法的 access_flags 上多了 ACC_SYNCHRONIZED 标志:
text复制public synchronized void syncMethod();
descriptor: ()V
flags: (0x0029)
