我们都知道,为了调用Object.wait()
,必须将此调用放在synchronized块中,否则抛出IllegalMonitorStateException
。但是制定此限制的原因是什么?我知道wait()
会释放监视器,但为什么我们需要通过使特定块同步来显式获取监视器,然后通过调用释放监视器wait()
?
如果可以在同步块外部调用wait()
,保留它的语义 - 暂停调用者线程会有什么潜在的损害?
答案 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();
}
}
这可能发生的事情:
消费者线程调用take()
并看到buffer.isEmpty()
。
在消费者线程继续调用wait()
之前,生产者线程出现并调用完整的give()
,即buffer.add(data); notify();
消费者线程现在将调用wait()
(错过刚刚调用的notify()
。
如果运气不好,生产者线程不会产生更多give()
,因为消费者线程永远不会醒来,而且我们有死锁。
一旦您理解了问题,解决方案显而易见:使用synchronized
确保永远不会在notify
和isEmpty
之间调用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()
将不会避免这种不一致的发生。
答案 3 :(得分:3)
如果您在wait()
之前执行不同步,则可能导致的问题如下:
makeChangeOnX()
并检查while条件,并且true
(x.metCondition()
返回false
,则表示x.condition
为{{ 1}})所以它会进入它。然后在false
方法之前,另一个帖子转到wait()
并将setConditionToTrue()
设置为x.condition
和true
。notifyAll()
方法(不受前一刻发生的wait()
的影响)。
在这种情况下,第一个线程将继续等待另一个线程执行notifyAll()
,但这可能不会再发生。但是如果你把
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 和缓存)。
如果您不将synchronized
与wait()
或notify()
一起使用,则另一个 可以进入相同的块,而不是等待监视器输入它。此外,当例如在没有同步块的情况下访问数组,另一个线程可能看不到对它的更改...实际上另一个线程将看到它的任何更改当它已经有一个副本时线程处理CPU核心的x级缓存(又名第一/第二/第三级缓存)中的数组。
但是,同步块只是奖牌的一面:如果您实际从非同步上下文访问同步上下文中的对象,则即使在同步块内,对象仍然无法同步,因为它保持缓存中对象的自有副本。我在这里写了这个问题:https://stackoverflow.com/a/21462631和When 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**
}
}