嵌套监视器锁死(Nested Monitor Lockout)

更新于 2025-12-28

Jakob Jenkov 2018-04-04

嵌套监视器锁死是如何发生的?

嵌套监视器锁死(Nested Monitor Lockout)是一种与死锁类似的问题。它通常按如下方式发生:

  1. 线程 1 对对象 A 加锁;
  2. 线程 1 在持有 A 的锁的同时,又对对象 B 加锁;
  3. 线程 1 决定在继续执行前等待另一个线程发来的信号;
  4. 线程 1 调用 B.wait(),从而释放了对 B 的锁,但没有释放对 A 的锁
  5. 线程 2 需要按顺序先锁住 A、再锁住 B,才能向线程 1 发送信号;
  6. 但线程 2 无法获取 A 的锁,因为线程 1 仍然持有它;
  7. 结果,线程 2 无限期地阻塞,等待线程 1 释放 A 的锁;
  8. 同时,线程 1 也无限期地阻塞,等待线程 2 发来的信号;而这个信号只有在线程 2 成功获取 A 和 B 的锁之后才能发送。

这就形成了一个循环依赖:线程 1 永远不会释放 A 的锁(因为它在等信号),而线程 2 永远无法发送信号(因为它拿不到 A 的锁)。最终,两个线程都陷入永久阻塞状态——这就是所谓的“嵌套监视器锁死”。

这听起来可能很理论化,但请看下面这个存在嵌套监视器锁死问题的朴素 Lock 实现:

// 存在嵌套监视器锁死问题的锁实现
public class Lock {
    protected MonitorObject monitorObject = new MonitorObject();
    protected boolean isLocked = false;

    public void lock() throws InterruptedException {
        synchronized (this) {
            while (isLocked) {
                synchronized (this.monitorObject) {
                    this.monitorObject.wait();
                }
            }
            isLocked = true;
        }
    }

    public void unlock() {
        synchronized (this) {
            this.isLocked = false;
            synchronized (this.monitorObject) {
                this.monitorObject.notify();
            }
        }
    }
}

注意 lock() 方法首先对 this 加锁,然后又对成员变量 monitorObject 加锁。如果 isLockedfalse,一切正常;但如果 isLockedtrue,调用 lock() 的线程就会在 monitorObject.wait() 处挂起。

问题在于:monitorObject.wait() 只会释放 monitorObject 上的锁,而不会释放 this 上的锁。也就是说,挂起的线程仍然持有 this 的监视器锁。

当最初获得锁的线程试图通过调用 unlock() 来释放锁时,它会尝试进入 unlock() 方法中的 synchronized(this) 块,但由于挂起线程仍持有 this 的锁,该线程会被阻塞。

简而言之:

  • 挂起的线程需要 unlock() 成功执行才能退出 lock() 方法;
  • 但任何线程都无法成功执行 unlock(),因为挂起线程尚未释放 this 的锁。

结果就是:所有调用 lock()unlock() 的线程都会无限期阻塞。这就是“嵌套监视器锁死”。


一个更现实的例子

你可能会说:“我绝不会写出上面那种锁!”确实,通常我们不会在一个内部监视器对象上调用 wait()notify(),而是直接在 this 上操作。

然而,在某些设计场景中,这种结构确实可能出现。例如,当你试图实现一个公平锁(Fair Lock) 时:你希望每个线程都在自己的“队列对象”上等待,以便能按顺序逐个唤醒它们。

下面是一个存在嵌套监视器锁死问题的公平锁实现:

// 存在嵌套监视器锁死问题的公平锁实现
public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread = null;
    private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();

    public void lock() throws InterruptedException {
        QueueObject queueObject = new QueueObject();
        synchronized (this) {
            waitingThreads.add(queueObject);
            while (isLocked || waitingThreads.get(0) != queueObject) {
                synchronized (queueObject) {
                    try {
                        queueObject.wait();
                    } catch (InterruptedException e) {
                        waitingThreads.remove(queueObject);
                        throw e;
                    }
                }
            }
            waitingThreads.remove(queueObject);
            isLocked = true;
            lockingThread = Thread.currentThread();
        }
    }

    public synchronized void unlock() {
        if (this.lockingThread != Thread.currentThread()) {
            throw new IllegalMonitorStateException(
                "Calling thread has not locked this lock");
        }
        isLocked = false;
        lockingThread = null;
        if (waitingThreads.size() > 0) {
            QueueObject queueObject = waitingThreads.get(0);
            synchronized (queueObject) {
                queueObject.notify();
            }
        }
    }
}

public class QueueObject {}

乍看之下,这段代码似乎没问题。但请注意:lock() 方法在两个嵌套的 synchronized 块中调用了 queueObject.wait() —— 外层同步于 this,内层同步于局部变量 queueObject

当线程调用 queueObject.wait() 时,它只释放了 queueObject 的锁,并未释放 this 的锁

同时,unlock() 方法被声明为 synchronized,等价于 synchronized(this)。这意味着:

  • 如果某个线程正在 lock() 中等待,它就一直持有 this 的锁;
  • 所有调用 unlock() 的线程都会因无法进入 synchronized(this) 而永久阻塞;
  • 而只有成功执行 unlock() 才能唤醒等待线程;
  • unlock() 又永远无法执行……

于是,这个“公平锁”反而导致了嵌套监视器锁死。

更好的公平锁实现,请参阅 《饥饿与公平性》 一文。


嵌套监视器锁死 vs 死锁

嵌套监视器锁死和死锁的结果非常相似:相关线程都永久阻塞,互相等待。

但二者成因不同:

  • 死锁:通常发生在两个线程以不同顺序获取多个锁时。
    例如:线程 1 锁 A 等 B,线程 2 锁 B 等 A。
    解决方法:始终以相同顺序加锁(锁排序)。

  • 嵌套监视器锁死:恰恰发生在两个线程以相同顺序获取锁的情况下。
    例如:线程 1 锁 A 和 B,然后释放 B 并等待信号;线程 2 需要同时持有 A 和 B 才能发信号。
    但线程 1 仍持有 A,导致线程 2 无法获取 A,也就无法发信号;而线程 1 又在等这个信号……

总结区别:

类型 等待内容
死锁 两个线程互相等待对方释放锁
嵌套监视器锁死 线程 1 持有锁 A 并等待线程 2 的信号;线程 2 需要锁 A 才能发送该信号