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


257

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

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

Answers:


232

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

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

if(!condition){
    wait();
}

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

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

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

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

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

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

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


3
这里也有详细的讨论,说的基本上是同一件事。coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/...

1
顺便说一句,如果您不忽略中断标志,循环也将进行检查Thread.interrupted()
bestsss

2
我仍然可以做类似的事情:while(!condition){synchronized(this){wait();}},这意味着即使在同步块中正确调用了wait(),在检查条件和等待之间仍然存在竞争。那么,此限制背后是否还有其他原因,也许是由于它在Java中的实现方式?
shrini1000 2012年

9
另一个令人讨厌的情况:condition为false,我们将进入wait(),然后另一个线程更改条件并调用notify()。因为我们还没有在wait()中,所以我们会错过这个notify()。换句话说,测试和等待以及更改和通知必须是原子的

1
@Nullpointer:如果它是可以原子地写的aa类型(例如直接在if子句中使用的布尔值),并且与其他共享数据没有相互依赖性,则可以声明其为volatile。但是您需要执行此操作或进行同步,以确保其他线程可以立即看到更新。
Michael Borgwardt

282

如果可以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块中执行来解决协议。


这篇文章已被重写为此处的文章:Java:为什么必须在同步块中调用wait


我猜想,此外,还要确保对条件所做的更改在wait()完成后立即可见。否则,由于已经调用notify(),因此也会出现死锁。
Surya Wijaya Madjid

有趣的是,但是请注意,由于wait()和notify()的“不可靠”性质,仅调用sync实际上并不能总是解决此类问题。在此处阅读更多信息:stackoverflow.com/questions/21439355/…。需要同步的原因在于硬件体系结构(请参见下面的答案)。
Marcus 2014年

但是如果return buffer.remove();在while之后添加while块wait();,它可以工作吗?
BobJiang

@BobJiang,不,可以出于除调用call之外的其他原因唤醒线程。换句话说,即使wait返回后缓冲区也可能为空。
aioobe '18

我只有Thread.currentThread().wait();maintry-catch包围的函数中InterruptedException。没有synchronized障碍,它给了我同样的例外IllegalMonitorStateException。是什么使它现在达到非法状态?它synchronized虽然在块内工作。
Shashwat

12

@Rollerball是正确的。将wait()被调用,从而使线程可以等待某些条件时,这种情况发生wait()调用发生时,线程被迫放弃其锁。
要放弃某些东西,您需要先拥有它。线程需要首先拥有锁。因此,需要在synchronized方法/块内调用它。

是的,如果您未检查synchronized方法/模块中的条件,则我确实同意上述所有关于潜在损坏/不一致的答案。但是,正如@ shrini1000所指出的那样,仅wait()在同步块内调用不会避免这种不一致的发生。

这是一本好书。


5
@Popeye正确地“说明”。您的评论对任何人都没有用。
罗恩侯爵

4

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

  1. 如果第一个线程进入makeChangeOnX()并检查while条件,则它是truex.metCondition()return false,Mean x.conditionis false),因此它将进入它的内部。然后在该wait()方法之前,另一个线程转到setConditionToTrue()并将其设置x.conditiontruenotifyAll()
  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;
    }
}

2

我们都知道,wait(),notify()和notifyAll()方法用于线程间通信。为了消除丢失的信号和虚假的唤醒问题,等待线程始终在某些情况下等待。例如-

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

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

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

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

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

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

谢谢,希望能弄清楚。


1

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

如果不synchronizedwait()或一起使用notify(),则另一个线程可以输入相同的块,而不必等待监视器输入它。此外,例如在访问没有同步块的数组时,另一个线程可能看不到它的变化...实际上,当另一个线程在x级缓存中已经具有该数组的副本,另一个线程看不到它的任何变化(也称为线程处理CPU内核的第1 / 2nd / 3rd级缓存。

但是,同步块只是奖牌的一面:如果您实际上是从非同步上下文访问同步上下文中的对象,则即使在同步块中,该对象也不会同步,因为它拥有自己的副本。对象在其缓存中。我在此处写过有关此问题的文章:https : //stackoverflow.com/a/21462631当锁包含非最终对象时,该对象的引用是否仍可以由另一个线程更改?

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

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


1
刚发现一个网站,对其进行了完美而深入的解释:javamex.com/tutorials/…–
Marcus

嗯..不确定我是否遵循。如果缓存是将wait和notify置于内部同步的唯一原因,那么为什么不将同步置于wait / notify的实现内部呢?
aioobe 2015年

很好的问题,因为等待/通知很可能是同步方法...也许Sun的前Java开发人员知道答案吗?请看一下上面的链接,也许这也将对您有所帮助:docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

原因可能是:在Java的早期,在执行这些多线程操作之前未调用sync时没有编译错误。相反,只有运行时错误(例如coderanch.com/t/239491/java-programmer-SCJP/certification/…)。也许他们真的以为@SUN,当程序员遇到这些错误时,正在与他们联系,这可能使他们有机会出售更多的服务器。它何时更改?也许是Java 5.0或6.0,但实际上我不记得是老实...
Marcus

TBH我看到您的答案有一些问题1)您的第二句话没有道理:线程锁定了哪个对象都没有关系。无论两个线程在哪个对象上同步,所有更改都将可见。2)您说另一个线程“不会”看到任何更改。这应该是“可能不会”。3)我不知道您为什么要建立第一级/第二级/第三级高速缓存...这里重要的是Java内存模型所说的内容,它是在JLS中指定的。虽然硬件体系结构可能有助于理解JLS 为什么要说明其作用,但严格来讲,在这种情况下它是无关紧要的。
aioobe

0

直接从这个 Java的甲骨文教程:

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


从作者提出的问题来看,问题作者似乎对我在本教程中引用的内容没有清楚的理解。此外,我的回答解释了“为什么”。
Rollerball

0

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

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


0

根据文档:

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

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


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**
       }
  }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.