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

时间:2010-05-06 08:08:59

标签: java multithreading concurrency wait

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

如果可以在同步块外部调用wait(),保留它的语义 - 暂停调用者线程会有什么潜在的损害?

10 个答案:

答案 0 :(得分:263)

  

如果可以在同步块之外调用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(),因为消费者线程永远不会醒来,而且我们有死锁。

  5. 一旦您理解了问题,解决方案显而易见:使用synchronized确保永远不会在notifyisEmpty之间调用wait

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


    link posted by @Willie中的一段很好地总结了它:

      

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

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


    此帖子已在此处重写为文章:Java: Why wait must be called in a synchronized block

答案 1 :(得分:218)

wait()仅在有notify()时才有意义,所以它总是与线程之间的通信有关,并且需要同步才能正常工作。有人可能会认为这应该是隐含的,但由于以下原因,这不会有所帮助:

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

if(!condition){
    wait();
}

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

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

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

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

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

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

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

答案 2 :(得分:11)

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

是的,如果您未在synchronized方法/区块内检查条件,我同意上述有关潜在损害/不一致的所有答案。但是正如@ shrini1000所指出的,只是在synchronized块中调用wait()将不会避免这种不一致的发生。

Here is a nice read..

答案 3 :(得分:3)

如果您在wait()之前执行同步,则可能导致的问题如下:

  1. 如果第一个帖子进入makeChangeOnX()并检查while条件,并且truex.metCondition()返回false,则表示x.condition为{{ 1}})所以它会进入它。然后在false方法之前,另一个帖子转到wait()并将setConditionToTrue()设置为x.conditiontrue
  2. 然后只有在那之后,第一个主题将进入他的notifyAll()方法(不受前一刻发生的wait()的影响)。 在这种情况下,第一个线程将继续等待另一个线程执行notifyAll(),但这可能不会再发生。
  3.   

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

    synchronized

答案 4 :(得分:2)

我们都知道wait(),notify()和notifyAll()方法用于进行内部线程 通信。摆脱错过的信号和虚假的唤醒问题,等待线程 总是在某些条件下等待。 e.g .-

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

然后通知线程集wasNotified变量为true并通知。

每个线程都有自己的本地缓存,所以所有的更改都先写在那里 然后逐渐升入主记忆。

如果在synchronized块中没有调用这些方法,则为wasNotified变量 不会被刷新到主内存中,并且会出现在线程的本地缓存中 所以等待的线程将继续等待信号,尽管它是通过通知重置的 线程。

要修复这些类型的问题,始终在synchronized块内调用这些方法 这确保了当同步块开始时,所有内容都将从main读取 内存并将在退出同步块之前刷新到主内存中。

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

谢谢,希望它澄清。

答案 5 :(得分:0)

直接来自this java oracle教程:

  

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

答案 6 :(得分:0)

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

如果您不将synchronizedwait()notify()一起使用,则另一个 可以进入相同的块,而不是等待监视器输入它。此外,当例如在没有同步块的情况下访问数组,另一个线程可能看不到对它的更改...实际上另一个线程看到它的任何更改它已经有一个副本时线程处理CPU核心的x级缓存(又名第一/第二/第三级缓存)中的数组。

但是,同步块只是奖牌的一面:如果您实际从非同步上下文访问同步上下文中的对象,则即使在同步块内,对象仍然无法同步,因为它保持缓存中对象的自有副本。我在这里写了这个问题:https://stackoverflow.com/a/21462631When a lock holds a non-final object, can the object's reference still be changed by another thread?

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

为什么编程类首先要从内存层次结构和CPU架构开始,这仍然是个谜。 &#34; Hello world&#34;在这里没有帮助。 ;)

答案 7 :(得分:0)

当您从对象t调用notify()时,java会通知特定的t.wait()方法。但是,java如何搜索并通知特定的等待方法。

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

答案 8 :(得分:0)

根据文档:

  

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

wait()方法仅表示它释放了对象上的锁。因此,对象将仅在同步块/方法内被锁定。如果线程在同步块之外,则意味着它没有被锁定;如果线程没有被锁定,那么您将在对象上释放什么?

答案 9 :(得分:0)

线程在监视对象(同步块使用的对象)上等待,单个线程的整个行程中可以有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**
       }
  }