为什么 wait() 必须始终在同步块中

2022-08-31 05:10:58

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

如果可以在同步块外部调用,保留其语义 - 挂起调用方线程,则潜在的损害是什么?wait()


答案 1

如果可以在同步块外部调用wait()并保留其语义 - 挂起调用方线程,那么潜在的损害是什么?

让我们用一个具体的例子来说明如果可以在同步块之外调用,我们会遇到什么问题。wait()

假设我们要实现一个阻塞队列(我知道,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();
    }
}

这是可能发生的事情:

  1. 使用者线程调用并看到 .take()buffer.isEmpty()

  2. 在使用者线程继续调用 之前,一个生产者线程出现并调用一个完整的 ,即wait()give()buffer.add(data); notify();

  3. 使用者线程现在将调用(并错过刚刚调用的线程)。wait()notify()

  4. 如果运气不好,生产者线程不会产生更多,因为消费者线程永远不会唤醒,并且我们有一个死锁。give()

一旦您了解了问题,解决方案就很明显:用于确保永远不会在 和 之间调用。synchronizednotifyisEmptywait

不谙述:此同步问题是普遍存在的。正如Michael Borgwardt所指出的,等待/通知完全是关于线程之间的通信,所以你总是会得到一个类似于上面描述的竞争条件。这就是强制执行“仅在同步内部等待”规则的原因。


@Willie发布的链接中的一段话很好地总结了这一点:

您需要绝对保证服务员和通知者同意谓词的状态。服务员在谓词进入睡眠状态之前的某个时间点检查谓词的状态,但它取决于谓词在进入睡眠状态时是否为 true 的正确性。这两个事件之间存在一段时间的漏洞,这可能会破坏程序。

生产者和消费者需要商定的谓词在上面的例子中。协议通过确保以块为单位执行等待和通知来解决。buffer.isEmpty()synchronized


这篇文章已经被重写为一篇文章:Java:为什么必须在同步块中调用等待


答案 2

只有当还有 一个 时才有意义,所以它总是关于线程之间的通信,并且需要同步才能正常工作。有人可能会争辩说这应该是隐含的,但这并没有真正的帮助,原因如下:wait()notify()

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

if(!condition){
    wait();
}

但是条件是由单独的线程设置的,因此为了正确完成此工作,您需要同步。

还有几处错误,仅仅因为你的线程退出等待并不意味着你正在寻找的条件是真实的:

  • 您可能会获得虚假唤醒(这意味着线程可以在没有收到通知的情况下从等待中唤醒),或者

  • 可以设置条件,但当等待线程唤醒(并重新获取监视器)时,第三个线程会再次使条件变为 false。

为了处理这些情况,您真正需要的始终是以下情况的一些变体:

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

更好的是,不要弄乱同步基元,而是使用包中提供的抽象。java.util.concurrent


推荐