为什么wait()始终处于同步块中

问题

我们都知道,为了调用Object.wait(),这个调用必须放在synchronized块中,否则抛出anIllegalMonitorStateException。但**这个限制的原因是什么?**我知道wait()释放了监视器,但为什么我们需要通过使特定块同步来显式获取监视器,然后通过调用wait()释放监视器?

如果有可能在同步块中调用wait(),保留它的语义 - 暂停调用程序线程,可能造成的损害是什么?


#1 热门回答(243 赞)

如果可以在同步块外部调用wait(),保留它的语义 - 暂停调用程序线程,那么可能造成的损害是什么?

让我们举例说明ifwait()可以在带有具体示例的同步块之外调用的问题。

假设我们要实现一个阻塞队列(我知道,API中已有一个:)

第一次尝试(没有同步)可能看起来像下面的行

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

这是可能发生的事情:

  • 消费者线程调用take()并看到buffer.isEmpty()。
  • 在消费者线程继续调用wait()之前,生成器线程出现并调用完整的give(),即buffer.add(data);通知();
  • 消费者线程现在将调用wait()(并且错过刚刚调用的notify())。
  • 如果运气不好,生产者线程不会产生更多的give(),因为消费者线程永远不会醒来,而且我们有一个死锁。

一旦你理解了这个问题,解决方案是显而易见的:始终执行58233163/notifyisEmpty/wait

没有详细说明:此同步问题是通用的。正如Michael Borgwardt所指出的那样,wait / notify是关于线程之间的通信的,所以你总是会遇到类似于上面描述的竞争条件。这就是强制执行"仅在内部同步"规则的原因。

link posted by @Willie中的一段很好地说明了这一点:

你需要绝对保证服务员和通知者同意谓词的状态。服务员在进入睡眠状态之前稍微检查一下谓词的状态,但这取决于谓词在进入睡眠状态时的正确性。这两个事件之间存在一段时间的脆弱性,这可能会破坏程序。

生产者和消费者需要达成一致的谓词在上面的例子中:buffer.isEmpty()。并且通过确保在synchronized块中执行等待和通知来解决协议。

这篇文章已被重写为文章:Java: Why wait must be called in a synchronized block


#2 热门回答(194 赞)

Await()在有anotify()时才有意义,所以它始终是线程之间的通信,需要同步才能正常工作。有人可能会认为这应该是隐含的,但这并不会有所帮助,原因如下:

在语义上,你永远不会只是wait()。你需要一些条件才能得到满足,如果不是,你就要等到它。所以你真正做的是

if(!condition){
    wait();
}

但是条件是由一个单独的线程设置的,所以为了使这个工作正常,你需要同步。

还有一些问题,只是因为你的线程退出等待并不意味着你正在寻找的条件是真的:

  • 你可以得到虚假的唤醒(意味着一个线程可以在没有收到通知的情况下从等待中醒来),或者
  • 条件可以设置,但是第三个线程在等待线程唤醒(并重新获取监视器)时再次使条件为false。

为了处理这些情况你真正需要的是以下一些变化:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

更好的是,不要混淆同步原语并使用java.util.concurrent包中提供的抽象。


#3 热门回答(10 赞)

@Rollerball是对的。调用了wait(),这样线程就可以等到发生这种情况时发生某种情况,线程被迫放弃锁定。
要放弃一些东西,你需要先拥有它。线程首先需要拥有锁。因此需要在asynchronized方法/块中调用它。

是的,如果你未在synchronized方法/区块内检查条件,我同意上述有关潜在损害/不一致的所有答案。但是正如@ shrini1000指出的那样,只需在同步块中调用wait()就不会避免这种不一致的发生。
Here is a nice read..