我们都知道,为了调用Object.wait(),必须将此调用放置在同步块中,否则抛出IllegalMonitorStateException。但是,进行此限制的原因是什么?我知道wait()释放了监视器,但是为什么我们需要通过使特定块同步来显式获取监视器,然后通过调用wait()释放监视器,如果可能在同步块外调用wait(),保留其语义-挂起调用者线程?

#1 楼

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

在语义上,您永远不会只是wait()。您需要满足一些条件,如果不是,请等到满足。因此,您真正要做的是

if(!condition){
    wait();
}


但是条件是由单独的线程设置的,因此为了正确执行此工作,需要同步。
还有很多其他问题,仅仅是因为线程退出等待并不意味着您要寻找的条件是正确的:


您可能会得到虚假的唤醒(意味着线程可以从等待中唤醒而无需接收通知),或者可以设置条件,但是第三个线程在等待线程唤醒时重新使条件成立(并重新获取监视器) )。

要处理这些情况,您真正需要的始终是此方法的一些变体:

同步原语完全可以使用java.util.concurrent软件包中提供的抽象。

评论


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

–user41871
2010年5月6日在8:28

顺便说一句,如果您不忽略中断标志,则循环也应检查Thread.interrupted()。

– bestsss
2011年11月15日在8:41

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

–shrini1000
2012年9月4日在7:48

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

–user3458
2014年6月18日12:50



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

– Michael Borgwardt
17年6月11日在20:18

#2 楼


如果可以在同步块外调用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();
    }
}


这可能会发生:


使用者线程调用take()并看到buffer.isEmpty()
在使用者线程继续调用wait()之前,生产者线程出现并调用完整的give(),即buffer.add(data); notify();
使用者线程将现在调用wait()(并错过了刚刚被调用的notify())。
如果不走运,生产者线程将不会由于产生更多的give()事实是,使用者线程永远不会醒来,并且我们陷入了死锁。一旦您了解了这个问题,解决方案就显而易见了:使用synchronized来确保在notifyisEmpty之间不会调用wait

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

@Willie发布的链接中的一段对其进行了很好的总结: />您需要绝对保证服务员和通知者就谓词的状态达成一致。服务员在进入睡眠之前的某个时候会稍稍检查谓词的状态,但是它的正确性取决于谓词在进入睡眠时为真。这两个事件之间存在一段时间的脆弱性,这可能会破坏程序。通过确保在buffer.isEmpty()块中执行wait和notify可以解决该协议。


本文已被重写为本文:Java:为什么必须在同步中调用wait块

评论


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

– Surya Wijaya Madjid
2012年7月31日14:21



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

–马库斯
2014年1月29日在21:25

但是如果添加return buffer.remove();在while块中但是在wait();之后,它有效吗?

–鲍勃·江
18年4月21日在2:04

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

– aioobe
18年4月21日在11:44

我只有Thread.currentThread()。wait();在由try-catch包围的InterruptedException的主要函数中。没有同步块,它给我同样的异常IllegalMonitorStateException。是什么使它现在达到非法状态?它虽然在同步块内工作。

– Shashwat
18年6月25日在9:44



#3 楼

@Rollerball是正确的。调用了wait(),以便线程可以在此wait()调用发生时等待某种条件发生,该线程被迫放弃其锁。线程需要先拥有该锁。
因此需要在一个synchronized方法/块内调用它。您没有在synchronized方法/块中检查条件。但是,正如@ shrini1000指出的那样,仅在同步块内调用wait()不会避免这种不一致的发生。

这是一本好书。.

评论


@Popeye正确解释。您的评论对任何人都没有用。

–user207421
2013年12月14日23:24

#4 楼

如果您在wait()之前未同步,可能会导致的问题如下:表示makeChangeOnX()true),因此它将进入内部。然后在x.metCondition()方法之前,另一个线程转到false并将x.condition设置为falsewait()
然后,第一个线程将进入他的setConditionToTrue()方法(不受之前几分钟发生的x.condition的影响) 。
在这种情况下,第一个线程将保持等待另一个线程执行true,但是可能不会再次发生。更改对象状态,将不会发生。


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;
    }
}


#5 楼

我们都知道,wait(),notify()和notifyAll()方法用于线程间的
通信。为了消除丢失的信号和虚假的唤醒问题,等待线程eg-

将wasNotified变量设置为true并进行通知。

每个线程都有其本地缓存,因此所有更改都首先写入那里,然后
然后逐步提升到主内存。如果未在同步块内调用这些方法,则wasNotified变量
将不会刷新到主内存中,并且将存在于线程的本地缓存中,因此等待线程将继续等待信号,尽管它已被重置。通知
线程。

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

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


谢谢,希望能弄清楚。

#6 楼

这基本上与硬件体系结构(即RAM和高速缓存)有关。输入它。此外,例如访问没有同步块的数组,另一个线程可能看不到它的变化...实际上,当另一个线程在x级缓存中(也就是1st / 2nd)已经拥有该数组的副本时,另一个线程将看不到它的任何变化。 / 3rd级缓存)处理线程的CPU内核。

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

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

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

评论


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

–马库斯
2014年1月30日16:00

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

– aioobe
15年1月27日在11:00

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

–马库斯
2015年6月11日15:22

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

–马库斯
2015年6月11日15:49

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

– aioobe
2015年6月11日20:30在

#7 楼

按照文档:


当前线程必须拥有此对象的监视器。线程释放此监视器的所有权。因此,对象将仅在同步块/方法内被锁定。如果线程在同步块之外,则意味着它未被锁定;如果未锁定,那么您将在该对象上释放什么?

#8 楼

直接从此Java oracle教程直接进行:


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


评论


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

–棒球
13年5月27日在12:42

#9 楼

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

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

#10 楼

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