297

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

wait()如果可以在同步块之外调用,保留它的语义——挂起调用者线程,那么潜在的损害是什么?

4

10 回答 10

303

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(),因为消费者线程永远不会唤醒,并且我们有一个死锁。

一旦你理解了这个问题,解决方案就很明显了:使用synchronized来确保notify永远不会在isEmptyand之间调用wait

无需赘述:这个同步问题是普遍的。正如 Michael Borgwardt 所指出的,等待/通知完全是关于线程之间的通信,因此您总是会遇到类似于上述情况的竞争条件。这就是强制执行“仅在同步内等待”规则的原因。


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

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

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


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

于 2010-05-06T08:43:23.047 回答
259

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

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

if(!condition){
    wait();
}

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

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

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

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

要处理这些情况,您真正需要的始终是以下几种情况:

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

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

于 2010-05-06T08:22:56.937 回答
14

@Rollerball 是对的。被wait()调用,以便线程可以等待某个条件发生,当这个wait()调用发生时,线程被迫放弃它的锁。
要放弃某些东西,你首先需要拥有它。线程需要首先拥有锁。因此需要在synchronized方法/块中调用它。

是的,如果您没有检查synchronized方法/块中的条件,我确实同意上述所有关于潜在损害/不一致的答案。然而,正如@shrini1000 所指出的,仅仅wait()在同步块内调用并不能避免这种不一致的发生。

这是一个很好的阅读..

于 2013-08-14T07:21:02.007 回答
4

之前不同步可能导致的问题wait()如下:

  1. 如果第一个线程进入makeChangeOnX()并检查 while 条件,并且它是truex.metCondition()返回false,意味着x.conditionfalse),那么它将进入其中。然后就在该wait()方法之前,另一个线程转到setConditionToTrue()并设置x.conditiontotruenotifyAll()
  2. 然后只有在那之后,第一个线程才会进入他的wait()方法(不受notifyAll()之前发生的事情的影响)。在这种情况下,第一个线程将等待另一个线程执行setConditionToTrue(),但这可能不会再次发生。

但是如果你把synchronized改变对象状态的方法放在前面,就不会发生这种情况。

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}
于 2016-02-26T14:24:07.727 回答
3

我们都知道wait()、notify()和notifyAll()方法用于线程间通信。为了摆脱丢失信号和虚假唤醒问题,等待线程总是在某些条件下等待。例如-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

然后通知线程将 wasNotified 变量设置为 true 并通知。

每个线程都有自己的本地缓存,因此所有更改首先写入那里,然后逐渐提升到主内存。

如果这些方法没有在同步块中调用,wasNotified 变量将不会被刷新到主内存中,并且将存在于线程的本地缓存中,因此等待线程将继续等待信号,尽管它已被通知线程重置。

为了解决这些类型的问题,这些方法总是在同步块中调用,这确保了当同步块启动时,所有内容都将从主内存中读取,并在退出同步块之前刷新到主内存中。

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

谢谢,希望它澄清。

于 2017-12-01T16:47:58.093 回答
2

这基本上与硬件架构(即RAM缓存)有关。

如果不与orsynchronized一起使用,另一个线程可以进入同一个块,而不是等待监视器进入它。此外,例如,当访问一个没有同步块的数组时,另一个线程可能看不到它的变化......实际上,当另一个线程已经在 x 级缓存中拥有数组的副本时,另一个线程不会看到它的任何变化(又名 1st/2nd/3rd-level 缓存)的线程处理 CPU 核心。wait()notify()

但是同步块只是奖牌的一方面:如果您实际上从非同步上下文访问同步上下文中的对象,即使在同步块中,该对象仍然不会同步,因为它拥有自己的副本缓存中的对象。我在这里写过这个问题:https ://stackoverflow.com/a/21462631和当锁持有一个非最终对象时,对象的引用仍然可以被另一个线程更改吗?

此外,我确信 x 级缓存是造成大多数不可重现的运行时错误的原因。这是因为开发人员通常不会学习低级的东西,例如 CPU 的工作方式或内存层次结构如何影响应用程序的运行:http ://en.wikipedia.org/wiki/Memory_hierarchy

为什么编程类不首先从内存层次结构和 CPU 架构开始,这仍然是一个谜。“Hello world”在这里无济于事。;)

于 2014-01-28T15:36:27.173 回答
1

线程等待监控对象(同步块使用的对象),单个线程全程可以有n个监控对象。如果线程在同步块外等待,则没有监控对象,并且其他线程通知访问监控对象,那么同步块外的线程如何知道它已被通知。这也是wait()、notify()和notifyAll()在对象类而不是线程类的原因之一。

基本上监控对象是所有线程的公共资源,监控对象只能在同步块中使用。

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
于 2020-06-21T18:16:49.020 回答
1

根据文档:

当前线程必须拥有该对象的监视器。线程释放此监视器的所有权。

wait()方法只是意味着它释放对象上的锁。因此对象将仅在同步块/方法中被锁定。如果线程在同步块之外意味着它没有被锁定,如果它没有被锁定,那么你会在对象上释放什么?

于 2019-06-03T07:29:34.747 回答
0

直接来自这个java oracle 教程:

当线程调用 d.wait 时,它必须拥有 d 的内在锁——否则会引发错误。在同步方法中调用 wait 是获取内在锁的一种简单方法。

于 2013-05-27T07:30:47.620 回答
0

当你调用notify()一个对象t时,Java 会通知一个特定的t.wait()方法。但是,Java 如何搜索和通知特定wait方法。

Java 只查看被对象锁定的同步代码块t。Java 无法搜索整个代码来通知特定的t.wait().

于 2016-03-10T08:23:53.693 回答