1. 概述

在Java中,我们有一个wait()/notify()API。这是在线程间同步的一种方式。为了使用这个API的方法,当前线程必须拥有被调用者(callee)的监视器。

在这篇教程中,我们将探讨为什么这种要求是有道理的。

2. wait()的工作原理

首先,我们需要简要了解一下Java中wait()的工作方式。根据JLS(Java语言规范),每个对象都有一个监视器。这意味着我们可以同步任何我们喜欢的对象。这可能不是一个好的决策,但这就是我们现在所拥有的。

有了这些,当我们调用wait()时,我们会隐式地做两件事。首先,我们将当前线程放入JVM内部的针对这个对象监视器的等待集合中。第二,一旦线程进入等待集合,我们(或者说JVM)会释放对这个对象的同步锁。这里需要澄清的是,这里的this指的是我们调用wait()方法的对象。

然后,当前线程就在集合中等待,直到另一个线程调用notify()notifyAll()在这个对象上。

3. 为何需要监视器所有权?

在上一节中,我们看到JVM做的第二件事是释放对this对象的同步锁。为了释放它,显然我们需要先拥有它。这样做的理由相对简单:为了防止丢失唤醒问题,wait()操作需要同步作为要求。这个问题本质上表示有一个等待线程错过了通知信号的情况。这主要由于线程间的竞态条件导致。让我们通过一个例子来模拟这个问题。

假设我们有以下Java代码:

private volatile boolean jobIsDone;

private Object lock = new Object();

public void ensureCondition() {
    while (!jobIsDone) {
        try {
            lock.wait();
        } 
        catch (InterruptedException e) {
            // ...
        }
    }
}

public void complete() {
    jobIsDone = true;
    lock.notify();
}

注意:这段代码在运行时会抛出IllegalMonitorStateException异常。这是因为,在两个方法中,我们在调用wait()notify()之前都没有请求锁定对象的监视器。因此,这段代码纯粹是为了演示和学习目的。

另外,假设我们有两个线程。所以,线程B正在执行有用的工作。一旦完成,线程B需要调用complete()方法来发出完成信号。我们还有一个线程A,它在等待B完成的工作。线程A通过调用ensureCondition()方法检查条件。由于Linux内核级别的偶然唤醒问题,条件检查发生在循环中,但这又是另一个话题。

4. 丢失唤醒问题的困境

让我们逐步分解我们的示例。假设线程A调用了ensureCondition()并进入while循环。它检查了条件,发现为假,于是进入了try块。因为在多线程环境中,另一个线程B可能会同时进入complete()方法。因此,B可以在A调用wait()之前调用设置volatile标志jobIsDonetrue并调用notify()

在这种情况下,如果线程B再也不会进入complete(),线程A将永远等待,从而所有与之关联的资源也会永远存在。这不仅会导致死锁,如果线程A持有其他锁,还可能导致内存泄漏,因为从线程A堆栈帧可达的对象将保持存活。这是因为线程A被认为是活跃的,因此不允许垃圾收集器回收在A方法中分配的对象。

5. 解决方案

因此,为了防止这种情况,我们需要同步。因此,执行前调用者必须拥有被调用者的监视器。所以,考虑到同步问题,让我们重写我们的代码:

private volatile boolean jobIsDone;
private final Object lock = new Object();

public void ensureCondition() {
    synchronized (lock) {
        while (!jobIsDone) {
            try {
                lock.wait();
            } 
            catch (InterruptedException e) { 
                // ...
            }
        }
    }
}

public void complete() {
    synchronized (lock) {
        jobIsDone = true;
        lock.notify();
    }
}

在这里,我们只是添加了一个synchronized块,在调用wait()/notify()API之前尝试获取lock对象的监视器。现在,如果B在A调用wait()之前执行complete()方法,我们就避免了丢失唤醒的问题。这是因为complete()方法只有在A没有获取lock对象监视器的情况下才能被B执行。因此,当complete()方法执行时,A无法检查条件。

6. 总结

在这篇文章中,我们讨论了Java中wait()方法为何需要同步。我们需要拥有被调用者的监视器以防止丢失唤醒的异常情况。如果我们不这样做,JVM将采取快速失败的方式并抛出IllegalMonitorStateException

如往常一样,这些示例的源代码可以在GitHub上找到。