Object.wait()
,必须将此调用放置在同步块中,否则抛出IllegalMonitorStateException
。但是,进行此限制的原因是什么?我知道wait()
释放了监视器,但是为什么我们需要通过使特定块同步来显式获取监视器,然后通过调用wait()
释放监视器,如果可能在同步块外调用wait()
,保留其语义-挂起调用者线程?#1 楼
仅当还存在wait()
时,notify()
才有意义,因此它始终与线程之间的通信有关,并且需要同步才能正常工作。有人可能会争辩说这应该是隐式的,但实际上并不会有所帮助,原因如下:在语义上,您永远不会只是
wait()
。您需要满足一些条件,如果不是,请等到满足。因此,您真正要做的是if(!condition){
wait();
}
但是条件是由单独的线程设置的,因此为了正确执行此工作,需要同步。
还有很多其他问题,仅仅是因为线程退出等待并不意味着您要寻找的条件是正确的:
您可能会得到虚假的唤醒(意味着线程可以从等待中唤醒而无需接收通知),或者可以设置条件,但是第三个线程在等待线程唤醒时重新使条件成立(并重新获取监视器) )。
要处理这些情况,您真正需要的始终是此方法的一些变体:
同步原语完全可以使用
java.util.concurrent
软件包中提供的抽象。#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
来确保在notify
和isEmpty
之间不会调用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
设置为false
和wait()
。然后,第一个线程将进入他的
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**
}
}
评论
这里也有详细的讨论,说的基本上是同一件事。 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