为什么wait()必须总是在同步块中

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

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

163860 次浏览

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

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

if(!condition){
wait();
}

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

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

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

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

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

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

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

如果有可能在同步块之外调用wait(),保留它的语义-挂起调用线程,那么潜在的损害是什么?< / em >

让我们来说明一下,如果可以在带有具体的例子的同步块之外调用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(). 0

  2. 在消费线程继续调用wait()之前,一个生产者线程出现并调用一个完整的give(),即buffer.add(data); notify();

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

  4. 如果不走运,生产者线程不会产生更多的give(),因为消费者线程从未被唤醒,我们有一个死锁。

一旦你理解了这个问题,解决方案是显而易见的:使用synchronized来确保在isEmptywait之间永远不会调用notify

不详细说明:这个同步问题是普遍存在的。正如Michael Borgwardt所指出的,wait/notify完全是关于线程之间的通信,所以您最终总是会得到类似于上面描述的竞争条件。这就是为什么强制执行“只在synchronized内部等待”规则的原因。


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

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

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


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

直接从 java oracle教程:

当一个线程调用d.wait时,它必须拥有d -的内在锁 否则抛出错误。在synchronized中调用等待 方法是获取内在锁的一种简单方法

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

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

这是一个很好的阅读..

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

如果你没有同时使用synchronizedwait()notify(),另一个线程可以进入同一个块,而不是等待监视器进入它。此外,当访问一个没有同步块的数组时,另一个线程可能看不到它的变化…实际上,另一个线程不会看到它的任何变化,它已经在处理CPU核心的线程的x级缓存(也就是第1 /2 /3级缓存)中有一个数组的副本。

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

此外,我确信x级缓存要对大多数不可重现的运行时错误负责。这是因为开发人员通常不了解底层的东西,比如CPU如何工作或内存层次结构如何影响应用程序的运行:http://en.wikipedia.org/wiki/Memory_hierarchy

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

如果你在wait()之前进行同步,可能会导致如下问题:

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

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

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

我们都知道wait(), notify()和notifyAll()方法用于线程间 通信。为摆脱误信号和伪唤醒问题,等待线程 总是等待一些条件。 例如,< / p >
boolean wasNotified = false;
while(!wasNotified) {
wait();
}

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

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

如果在同步块中没有调用这些方法,则变量为wasNotified 不会被刷新到主内存中,而是在线程的本地缓存中 因此,等待线程将继续等待信号,尽管它已通过通知重置 线程。< / p > 要修复这些类型的问题,这些方法总是在同步块中调用 这确保当同步块开始时,那么一切将从主读取 内存,并将在退出同步块之前被刷新到主内存。< / p >
synchronized(monitor) {
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
}

谢谢,希望它能澄清。

根据文档:

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

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

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