一尘不染

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

java

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

如果可以wait()在同步块之外调用并保留其语义-挂起调用者线程,可能造成什么损害?


阅读 663

收藏
2020-02-29

共2个答案

一尘不染

wait()只有在还存在时,A才有意义notify(),因此它始终与线程之间的通信有关,并且需要同步才能正常工作。有人可能会争辩说这应该是隐式的,但这并没有真正的帮助,原因如下:

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

if(!condition){
    wait();
}

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

还有其他一些问题,仅仅是因为线程退出等待并不意味着你要寻找的条件是正确的:

你可能会得到虚假的唤醒(这意味着线程可以从等待中唤醒而从未收到通知),或者

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

为了处理这些情况,你真正需要的始终是这种变化:

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

更好的是,根本不要弄乱同步原语,而要使用java.util.concurrent软件包中提供的抽象。

2020-02-29
一尘不染

如果可以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(),生产者线程会出现并调用full give(),即buffer.add(data); notify();
  3. 使用者线程现在将调用wait()(并且错过了notify()刚刚被调用的线程)。
  4. 如果运气不好,生产者线程将不会产生更多give()的结果,因为消费者线程永远不会醒来,而且我们陷入了僵局。
    理解了问题之后,解决方案就显而易见了:用于synchronized确保notifyisEmpty和之间不调用wait

无需赘述:同步问题是普遍的。正如Michael Borgwardt指出的那样,等待/通知完全是关于线程之间的通信的,所以你总是会遇到与上述情况类似的竞争状态。这就是为什么强制执行“仅在同步中等待”规则的原因。

@Willie发布的链接中的一段对其进行了很好的总结:

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

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

2020-02-29