JVM监视器锁是什么
1. 监视器锁的核心组成部分
每个 Java 对象在 JVM 层面都与一个监视器(Monitor)相关联。这个监视器主要由以下三个部分组成:
- 一个特殊的所有者(Owner)字段: 这个字段用于记录当前哪个线程持有了该监视器锁。在任意时刻,这个字段最多只能指向一个线程,或者为
null(表示锁未被任何线程持有)。 - 一个入口集(Entry Set): 这是一个等待队列,用于存放所有尝试获取但尚未获得该监视器锁的阻塞线程。当一个线程试图进入一个已被占用的
synchronized代码块时,它就会被放入这个入口集中等待。 - 一个等待集(Wait Set): 这是另一个等待队列,用于存放调用了该对象
wait()方法的线程。这些线程是主动释放了监视器锁并进入等待状态,等待被其他线程通过notify()或notifyAll()唤醒。
2. 监视器锁的工作流程
当一个线程遇到 synchronized 关键字时,其工作流程如下:
-
尝试获取锁:
- 线程会尝试获取与该对象关联的监视器锁。
- 它会检查监视器的
Owner字段是否为null。 - 如果为
null,表示锁是空闲的,该线程将成功获取锁,并将监视器的Owner字段设置为自己。然后,它可以进入synchronized代码块执行。
-
获取锁失败(锁已被占用):
- 如果线程发现监视器的
Owner字段指向的是另一个线程,表示锁已被占用。 - 此时,该线程的执行会受阻,它将被放入该监视器的入口集(Entry Set)中,并进入阻塞状态(BLOCKED)。
- 如果线程发现监视器的
-
释放锁:
- 当持有锁的线程执行完毕
synchronized代码块或方法后(无论是正常退出还是因异常退出),它会自动释放这个监视器锁。 - 释放锁的操作包括将监视器的
Owner字段重新设置为null。
- 当持有锁的线程执行完毕
-
唤醒等待的线程:
- 锁被释放后,JVM 会从该监视器的入口集(Entry Set)中唤醒一个或多个等待的线程(具体唤醒哪个线程是不确定的,取决于 JVM 的调度策略)。
- 被唤醒的线程会再次尝试获取锁,这个过程会产生竞争,最终只有一个线程能成功获取锁并继续执行。
3. 监视器锁与 wait() / notify() / notifyAll() 的交互
监视器锁不仅实现了互斥(Mutual Exclusion),还支持线程间的协作,这就要依靠 wait()、notify() 和 notifyAll() 这三个方法。
-
wait():- 一个线程必须首先持有对象的监视器锁,才能调用该对象的
wait()方法。 - 调用
wait()后,该线程会立即释放它持有的监视器锁。 - 然后,该线程会被放入该监视器的等待集(Wait Set)中,并进入等待状态(WAITING 或 TIMED_WAITING)。
- 它会一直在此等待,直到被其他线程唤醒。
- 一个线程必须首先持有对象的监视器锁,才能调用该对象的
-
notify()/notifyAll():- 一个线程也必须首先持有对象的监视器锁,才能调用该对象的
notify()或notifyAll()方法。 notify()会从等待集(Wait Set)中随机选择一个线程,并将其唤醒。被唤醒的线程会被移动到入口集(Entry Set)中,准备重新竞争锁。notifyAll()则会唤醒等待集(Wait Set)中所有的线程,并将它们全部移动到入口集(Entry Set)中,让它们一起去竞争锁。- 非常重要的一点:调用
notify()或notifyAll()的线程并不会立即释放锁。它必须等到自己执行完synchronized代码块后,才会释放锁。这时,那些被唤醒并移动到入口集的线程才有机会去获取锁。
- 一个线程也必须首先持有对象的监视器锁,才能调用该对象的