monitorexit指令在字节码中出现两次是JVM为保障锁安全释放而设计的强制机制:一次用于正常流程退出,一次通过异常表覆盖所有异常路径,确保无论同步块是否抛出异常(包括RuntimeException、Error等),锁都能被及时释放,避免死锁。

Java里monitorexit指令在字节码中出现两次,不是冗余或bug,而是JVM为保障锁安全释放所设计的强制机制——它确保无论同步块是正常结束,还是因异常(包括RuntimeException、Error甚至未捕获的受检异常)中途退出,锁都能被及时释放。
一次对应正常流程,一次覆盖所有异常路径
编译器在生成synchronized代码的字节码时,会插入两条monitorexit指令:
- 第一条位于同步块末尾,供线程执行完代码后自然退出时调用;
- 第二条被嵌入到异常表(Exception table)中,其触发范围覆盖整个
monitorenter到第一个monitorexit之间的全部字节码。只要该范围内抛出任何异常,JVM就会跳转到这条monitorexit执行释放逻辑。
依赖异常表而非finally,不靠程序员干预
这个机制完全由JVM在字节码层面实现,和源码里有没有try-catch-finally无关:
- 即使你没写
finally,JVM也会通过异常表自动兜底; - 它不依赖
finally语义,因此不会因System.exit()、线程中断或JVM崩溃而失效; - 对比
Lock接口,synchronized的这种双monitorexit设计,本质是把“释放锁”这件事交给了JVM硬性保证,避免了手动unlock()遗漏导致的死锁风险。
从字节码看具体结构
反汇编后常见结构如下(简化示意):
立即学习“Java免费学习笔记(深入)”;
6: monitorenter<br> 7: ... 同步块内字节码 ...<br> 15: monitorexit ← 正常退出路径<br> 17: goto 25<br> 20: astore_2 ← 异常处理器入口<br> 21: aload_0<br> 22: monitorexit ← 异常退出路径<br> 23: aload_2<br> 24: athrow<br>Exception table:<br> from 7 to 17 using 20 any<br> from 20 to 23 using 20 any
异常表明确声明:从字节码位置7到17之间若发生任意异常(any),就跳转到20开始执行;而20之后的逻辑,核心就是先monitorexit再重新抛出异常。
这是JVM级保障,不是语言语法糖
synchronized的锁释放行为,本质上是JVM对monitor对象状态的底层管理:
- 每次
monitorexit都会使监视器计数器减1,当计数归零,_owner字段清空,锁才真正释放; - 两个出口都执行同一套释放逻辑,保证原子性与一致性;
- 这种设计早在JDK 1.0就已确立,至今未变,属于Java内存模型(JMM)可依赖的确定性行为。


















