monitorenter /moniterexit
在 Java 中,同步代码块通过 monitorenter
和 moniterexit
字节码指令来保证同一时刻只有一个线程能够进入同步代码块,下面从监视器(Monitor)的概念、monitorenter
和 moniterexit
的工作流程以及锁的状态几个方面详细阐述其实现原理。
监视器(Monitor)的概念
每个 Java 对象都可以关联一个监视器(Monitor),监视器是 Java 中实现同步的基础,它本质上是一个同步工具,也可以理解为一种锁的实现。监视器有一个重要的属性:进入数(Entry Count),用于记录有多少个线程进入了该监视器。当进入数为 0 时,表示该监视器没有被任何线程持有;当有线程进入监视器时,进入数会加 1;当线程退出监视器时,进入数会减 1。
monitorenter
指令的工作流程
当 JVM 执行到 monitorenter
指令时,会按照以下步骤尝试获取对象关联的监视器:
- 检查监视器的进入数:首先检查对象关联的监视器的进入数是否为 0。
- 如果进入数为 0,说明该监视器没有被任何线程持有,当前线程可以成功获取该监视器,将进入数设置为 1,同时该线程成为监视器的所有者,然后继续执行同步代码块中的代码。
- 如果进入数不为 0,说明已经有其他线程持有了该监视器,此时当前线程会被阻塞,进入等待队列,直到监视器的进入数变为 0 时,再尝试获取监视器。
- 可重入性:如果当前线程已经是该监视器的所有者,再次执行
monitorenter
指令时,监视器的进入数会加 1,而不是被阻塞。这就是 Java 中synchronized
的可重入特性,允许同一个线程多次进入同一个同步代码块,避免了死锁的发生。
moniterexit
指令的工作流程
当 JVM 执行到 moniterexit
指令时,会按照以下步骤释放对象关联的监视器:
- 减少进入数:将监视器的进入数减 1。
- 判断是否释放监视器:如果进入数减为 0,说明当前线程已经完全退出了该同步代码块,会释放该监视器,唤醒等待队列中等待获取该监视器的线程,让它们有机会竞争获取监视器。
- 异常处理:在同步代码块中,如果发生异常,也会执行
moniterexit
指令,确保监视器能够被正确释放,避免出现死锁的情况。这就是为什么在字节码中会有两个moniterexit
指令,一个用于正常退出同步代码块,另一个用于异常退出同步代码块。
示例代码及字节码分析
public class SynchronizedBlockExample {
public void test() {
Object lock = new Object();
synchronized (lock) {
// 同步代码
}
}
}
对应的字节码片段:
public void test();
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: monitorenter // 进入同步块,尝试获取监视器
10: aload_1
11: monitorexit // 正常退出同步块,释放监视器
12: goto 20
15: astore_2
16: aload_1
17: monitorexit // 异常退出同步块,释放监视器
18: aload_2
19: athrow
20: return
通过 monitorenter
和 moniterexit
指令以及监视器的进入数机制,Java 保证了同一时刻只有一个线程能够进入同步代码块,从而实现了线程同步。