Skip to content

JVM监视器锁是什么

1. 监视器锁的核心组成部分

每个 Java 对象在 JVM 层面都与一个监视器(Monitor)相关联。这个监视器主要由以下三个部分组成:

  1. 一个特殊的所有者(Owner)字段: 这个字段用于记录当前哪个线程持有了该监视器锁。在任意时刻,这个字段最多只能指向一个线程,或者为 null(表示锁未被任何线程持有)。
  2. 一个入口集(Entry Set): 这是一个等待队列,用于存放所有尝试获取但尚未获得该监视器锁的阻塞线程。当一个线程试图进入一个已被占用的 synchronized 代码块时,它就会被放入这个入口集中等待。
  3. 一个等待集(Wait Set): 这是另一个等待队列,用于存放调用了该对象 wait() 方法的线程。这些线程是主动释放了监视器锁并进入等待状态,等待被其他线程通过 notify()notifyAll() 唤醒。

2. 监视器锁的工作流程

当一个线程遇到 synchronized 关键字时,其工作流程如下:

  1. 尝试获取锁:

    • 线程会尝试获取与该对象关联的监视器锁。
    • 它会检查监视器的 Owner 字段是否为 null
    • 如果为 null,表示锁是空闲的,该线程将成功获取锁,并将监视器的 Owner 字段设置为自己。然后,它可以进入 synchronized 代码块执行。
  2. 获取锁失败(锁已被占用):

    • 如果线程发现监视器的 Owner 字段指向的是另一个线程,表示锁已被占用。
    • 此时,该线程的执行会受阻,它将被放入该监视器的入口集(Entry Set)中,并进入阻塞状态(BLOCKED)。
  3. 释放锁:

    • 当持有锁的线程执行完毕 synchronized 代码块或方法后(无论是正常退出还是因异常退出),它会自动释放这个监视器锁。
    • 释放锁的操作包括将监视器的 Owner 字段重新设置为 null
  4. 唤醒等待的线程:

    • 锁被释放后,JVM 会从该监视器的入口集(Entry Set)中唤醒一个或多个等待的线程(具体唤醒哪个线程是不确定的,取决于 JVM 的调度策略)。
    • 被唤醒的线程会再次尝试获取锁,这个过程会产生竞争,最终只有一个线程能成功获取锁并继续执行。

3. 监视器锁与 wait() / notify() / notifyAll() 的交互

监视器锁不仅实现了互斥(Mutual Exclusion),还支持线程间的协作,这就要依靠 wait()notify()notifyAll() 这三个方法。

  • wait():

    1. 一个线程必须首先持有对象的监视器锁,才能调用该对象的 wait() 方法。
    2. 调用 wait() 后,该线程会立即释放它持有的监视器锁。
    3. 然后,该线程会被放入该监视器的等待集(Wait Set)中,并进入等待状态(WAITING 或 TIMED_WAITING)。
    4. 它会一直在此等待,直到被其他线程唤醒。
  • notify() / notifyAll():

    1. 一个线程也必须首先持有对象的监视器锁,才能调用该对象的 notify()notifyAll() 方法。
    2. notify() 会从等待集(Wait Set)中随机选择一个线程,并将其唤醒。被唤醒的线程会被移动到入口集(Entry Set)中,准备重新竞争锁。
    3. notifyAll() 则会唤醒等待集(Wait Set)中所有的线程,并将它们全部移动到入口集(Entry Set)中,让它们一起去竞争锁。
    4. 非常重要的一点:调用 notify()notifyAll() 的线程并不会立即释放锁。它必须等到自己执行完 synchronized 代码块后,才会释放锁。这时,那些被唤醒并移动到入口集的线程才有机会去获取锁。