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标志jobIsDone
为true
并调用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上找到。