为什么必须等待()始终处于同步块

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

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

wait()只有在notify() wait()时才有意义,所以它总是关于线程之间的通信,需要同步才能正常工作。 有人可能会争辩说,这应该是隐含的,但这不会真的有帮助,原因如下:

在语义上,你永远不要wait() 。 你需要一些条件,如果不是,你就等着。 所以你真正做的是

 if(!condition){ wait(); } 

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

还有更多的事情是错误的,只是因为你的线程退出等待并不意味着你正在寻找的条件是正确的:

  • 你可以得到虚假的唤醒(意味着一个线程可以从没有收到通知的等待中唤醒),或者

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

为了处理这些情况,你真正需要的总是这样的一些变化:

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

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

如果可以在同步块之外调用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(); buffer.add(data); notify();

  3. 消费者线程现在将调用wait() (并错过刚刚调用的notify() )。

  4. 如果不幸的话,生产者线程不会产生更多的give()因为消费者线程永不会被唤醒,而且我们有一个死锁。

一旦你明白这个问题,解决方案是显而易见的:总是执行give / notifyisEmpty / wait自动。

没有进入细节:这个同步问题是普遍的。 正如Michael Borgwardt所指出的,wait / notify是关于线程之间的通信的,所以你总是会得到与上面描述的类似的竞争状态。 这就是为什么“只在等待内同步”规则强制执行。


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

你需要绝对保证服务员和通知者对谓词的状态达成一致。 服务员在进入睡眠之前稍微检查一下谓词的状态,但是当它进入睡眠状态时,谓词的正确性依赖于它。 这两个事件之间有一段脆弱的时期,可能会打破这个计划。

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


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

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

是的,如果您没有在synchronized方法/程序块中检查条件,我同意所有上述有关潜在损害/不一致的答案。 然而正如@ shrini1000指出的那样,只要在synchronized块中调用wait()就不会避免这种不一致的发生。

这是一个很好的阅读..

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

  1. 如果第一个线程进入makeChangeOnX()并检查while条件,并且它是truex.metCondition()返回false ,意味着x.conditionfalse ),所以它会进入它。 然后,在wait()方法之前,另一个线程转到setConditionToTrue()并将x.condition设置为true并将notifyAll()
  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; } } 

直接从这个 java的oracle教程:

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

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

如果不同时使用wait()notify() synchronized ,另一个线程可能会进入同一个块,而不是等待监视器输入。 而且,当访问一个没有同步块的数组时,另一个线程可能不会看到它的变化…实际上,另一个线程在x级缓存中已经有一个数组的副本 不会看到任何变化即第一/第二/第三级高速缓存)的线程处理CPU内核。

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

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

它仍然是一个谜,为什么编程类首先不以内存层次结构和CPU架构开始。 “你好世界”在这里不会有帮助。 ;)

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

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