1. 一次模拟面试的反问:你说"锁升级",可锁到底存在哪个比特里
最近帮几个朋友做模拟面试,发现一个特别有意思的现象:只要问到 synchronized 底层原理,十个候选人里至少有七个能说出"JDK 6 之后有锁升级""偏向锁、轻量级锁、重量级锁"这条主线。但当我继续追问一句"那你对象头里的 Mark Word 一共几个比特?偏向锁和无锁状态分别长什么样?"——当场卡壳的占一半以上。
这个细节恰恰是 synchronized 底层原理真正的分水岭。背过整体流程的人很多,能把每个状态在内存里的二进制布局讲清楚的人很少。而面试官问 synchronized,通常并不指望你把 HotSpot 源码逐行背出来,他真正想确认的是:你对"锁"这个抽象概念有没有落到底层的能力——它到底锁住了什么,用什么东西记录了持有者,竞争发生时 JVM 在用户态和内核态之间做了哪些事情。
先说三句话结论,方便你后面带着主线往下看:
synchronized的一切行为围绕对象头里的 Mark Word 展开,Mark Word 会随着锁状态变化而重用存储语义。- 锁升级本质是:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,从左到右成本递增,触发条件完全由竞争程度决定。
- 重量级锁最终依赖操作系统底层的互斥量来阻塞线程,这也是"重量"二字的来源。
还要补充一个很多人忽略的前提:JDK 8、JDK 11 和 JDK 17 的锁行为并不一样。JDK 15 之后偏向锁被默认禁用,JDK 18 之后被标记废弃,这对你回答"偏向锁怎么撤销"这类问题影响很大。所以我建议先确认你面试用的 JDK 版本,再决定话术怎么组织。下文我会把 JDK 8 的经典路径和新版本 JDK 的简化路径都讲清楚,这样不管面试官用的是哪条线,你都能接住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从字节码入手:synchronized 编译后到底"翻译"成了什么
要理解 synchronized 的底层原理,绕不开的一个动作是反编译。很多人在源码层面看 synchronized 觉得没什么可研究的,就是个关键字,但编译成字节码之后,它的实现痕迹暴露得很明显。
2.1 同步代码块:monitorenter 与 monitorexit 的成对出现
先看一段最普通的代码:
java复制public class SyncDemo {
private final Object lock = new Object();
public void hello() {
synchronized (lock) {
System.out.println("hello synchronized");
}
}
}
编译后用 javap -c -v SyncDemo.class 反编译,关键的指令序列是这样的:
java复制public void hello();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #7 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #19 // String hello synchronized
12: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 15 20 any
进入同步块时执行 monitorenter,正常退出同步块时执行 monitorexit。注意,为了应对同步块里抛异常导致锁释放不了的情况,编译器在字节码层面生成了两条 monitorexit 路径:一条是正常路径(偏移量 16),一条是异常路径(偏移量 22)。异常表里记录了 7 ~ 15 这段范围如果抛出任何异常,就跳到偏移量 20 的处理逻辑,先执行 monitorexit 释放锁,再把异常重新抛出。
这个设计很多人第一次看会忽略,但它是"synchronized 在异常情况下也能保证锁一定释放"的底层证据。JVM 不是靠 try/finally 帮你兜底,而是编译器直接生成异常处理指令来保证的。
