synchronized代码块通过monitorenter和monitorexit指令实现,编译器自动插入并确保配对;同步方法则依赖ACC_SYNCHRONIZED标志,由JVM隐式处理锁逻辑。

monitorenter 和 monitorexit 指令是核心
synchronized 的字节码实现不依赖函数调用,而是由 JVM 直接插入 monitorenter 与 monitorexit 指令。这两条指令控制 Monitor 的获取与释放,不是 Java 方法,无法被重写或拦截。
编译器(如 javac)在遇到 synchronized 代码块时,会在块入口插入一条 monitorenter,在所有可能的出口(正常结束、异常跳转)插入对应数量的 monitorexit。JVM 运行时据此执行锁逻辑。
- 每个
monitorenter必须配对至少一个monitorexit,否则校验失败,类加载会抛出VerifyError - 同步方法不显式生成这两条指令,而是通过方法标志位
ACC_SYNCHRONIZED标记,JVM 在调用该方法前自动执行等效的monitorenter,返回前执行monitorexit - 异常出口的
monitorexit不靠手动编写,而是由编译器根据异常表(exception table)自动生成,确保不会漏解锁
同步代码块 vs 同步方法的字节码差异
两者语义一致,但字节码表现不同,直接影响你用 jclasslib 或 javap -v 查看时的观察结果:
-
synchronized(obj) { ... }:你会在字节码中明确看到monitorenter和monitorexit指令,操作数栈顶必须是锁对象引用(即obj) -
public synchronized void m() { ... }:方法属性里有ACC_SYNCHRONIZED标志,字节码中**看不到**任何 lock 相关指令——锁行为完全由 JVM 调用机制隐式完成 - 静态同步方法同样走
ACC_SYNCHRONIZED,但锁对象是Demo.class,不是this;这点在字节码里也完全不体现,只靠方法签名和修饰符推断
为什么不能靠字节码判断“锁的是谁”
字节码本身不记录锁对象的身份信息,只记录“如何取锁”。真正决定锁对象的是 Java 源码上下文,而非指令操作数:
-
monitorenter指令本身不携带“this”或“Demo.class”的硬编码,它只是弹出栈顶引用并尝试对该对象加锁 - 所以你在
javap输出里看到monitorenter,但不知道它锁的是this、lock还是getClass(),必须回溯到源码中那行synchronized(...)的括号内容 - 同理,
ACC_SYNCHRONIZED标志不区分实例/静态——它是方法级标记,锁对象类型(实例 or Class)由方法是否为static决定,而这个信息在常量池的Methodref或方法描述符中,不在标志位里
容易忽略的细节:异常路径下的 monitorexit 是怎么插进去的
很多人以为只要写个 try-finally 就能模拟 synchronized,但 JVM 的异常处理机制比手写更严密:
- 编译器为每个
synchronized块生成的异常表(exception table)会覆盖所有可能抛出异常的字节码范围,并将控制流导向一个专门的monitorexit序列 - 哪怕方法里用了
return、break、continue或未捕获的 RuntimeException,JVM 都能保证对应monitorexit执行,这是字节码验证器强制保障的 - 手写
try-finally+Lock.lock()/unlock()时,如果lock()抛异常,unlock()根本不会执行——而monitorenter失败时根本不会进入同步块,也就没有monitorexit可执行,不存在“加锁失败却要解锁”的问题
真正难的不是看懂 monitorenter,而是理解它背后绑定的 Monitor 生命周期、对象头 Mark Word 的状态切换,以及这些如何被 JIT 编译器进一步优化。字节码只是起点,不是终点。


















