我们都知道,为了调用Object.wait()
,这个调用必须放在同步块中,否则IllegalMonitorStateException
抛出an。但是做出这个限制的原因是什么?我知道wait()
释放监视器,但是为什么我们需要通过使特定块同步来显式获取监视器,然后通过调用释放监视器wait()
?
wait()
如果可以在同步块之外调用,保留它的语义——挂起调用者线程,那么潜在的损害是什么?
我们都知道,为了调用Object.wait()
,这个调用必须放在同步块中,否则IllegalMonitorStateException
抛出an。但是做出这个限制的原因是什么?我知道wait()
释放监视器,但是为什么我们需要通过使特定块同步来显式获取监视器,然后通过调用释放监视器wait()
?
wait()
如果可以在同步块之外调用,保留它的语义——挂起调用者线程,那么潜在的损害是什么?
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
and之间调用wait
。
无需赘述:这个同步问题是普遍的。正如 Michael Borgwardt 所指出的,等待/通知完全是关于线程之间的通信,因此您总是会遇到类似于上述情况的竞争条件。这就是强制执行“仅在同步内等待”规则的原因。
@Willie 发布的链接中的一段很好地总结了它:
您需要绝对保证服务员和通知者就谓词的状态达成一致。服务员在进入睡眠之前的某个时间点检查谓词的状态,但它的正确性取决于谓词在进入睡眠时为真。这两个事件之间存在一段时间的漏洞,可能会破坏程序。
生产者和消费者需要达成一致的谓词在上面的例子buffer.isEmpty()
中。并且通过确保分synchronized
块执行等待和通知来解决协议。
这篇文章在这里被改写为一篇文章:Java:为什么必须在同步块中调用等待
Await()
仅在还有 a 时才有意义notify()
,因此它始终与线程之间的通信有关,并且需要同步才能正常工作。有人可能会争辩说这应该是隐含的,但这并没有真正的帮助,原因如下:
从语义上讲,你永远不会只是wait()
. 你需要满足一些条件,如果不满足,你就等到满足。所以你真正要做的是
if(!condition){
wait();
}
但是条件是由一个单独的线程设置的,所以为了让这个工作正常,你需要同步。
还有一些问题,仅仅因为你的线程退出等待并不意味着你正在寻找的条件是真的:
您可能会收到虚假唤醒(这意味着线程可以在没有收到通知的情况下从等待中唤醒),或者
可以设置条件,但是在等待线程唤醒(并重新获取监视器)时,第三个线程再次使条件为假。
要处理这些情况,您真正需要的始终是以下几种情况:
synchronized(lock){
while(!condition){
lock.wait();
}
}
更好的是,根本不要弄乱同步原语,而是使用java.util.concurrent
包中提供的抽象。
@Rollerball 是对的。被wait()
调用,以便线程可以等待某个条件发生,当这个wait()
调用发生时,线程被迫放弃它的锁。
要放弃某些东西,你首先需要拥有它。线程需要首先拥有锁。因此需要在synchronized
方法/块中调用它。
是的,如果您没有检查synchronized
方法/块中的条件,我确实同意上述所有关于潜在损害/不一致的答案。然而,正如@shrini1000 所指出的,仅仅wait()
在同步块内调用并不能避免这种不一致的发生。
之前不同步可能导致的问题wait()
如下:
makeChangeOnX()
并检查 while 条件,并且它是true
(x.metCondition()
返回false
,意味着x.condition
是false
),那么它将进入其中。然后就在该wait()
方法之前,另一个线程转到setConditionToTrue()
并设置x.condition
totrue
和notifyAll()
。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;
}
}
我们都知道wait()、notify()和notifyAll()方法用于线程间通信。为了摆脱丢失信号和虚假唤醒问题,等待线程总是在某些条件下等待。例如-
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
然后通知线程将 wasNotified 变量设置为 true 并通知。
每个线程都有自己的本地缓存,因此所有更改首先写入那里,然后逐渐提升到主内存。
如果这些方法没有在同步块中调用,wasNotified 变量将不会被刷新到主内存中,并且将存在于线程的本地缓存中,因此等待线程将继续等待信号,尽管它已被通知线程重置。
为了解决这些类型的问题,这些方法总是在同步块中调用,这确保了当同步块启动时,所有内容都将从主内存中读取,并在退出同步块之前刷新到主内存中。
synchronized(monitor) {
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
}
谢谢,希望它澄清。
这基本上与硬件架构(即RAM和缓存)有关。
如果不与orsynchronized
一起使用,另一个线程可以进入同一个块,而不是等待监视器进入它。此外,例如,当访问一个没有同步块的数组时,另一个线程可能看不到它的变化......实际上,当另一个线程已经在 x 级缓存中拥有数组的副本时,另一个线程不会看到它的任何变化(又名 1st/2nd/3rd-level 缓存)的线程处理 CPU 核心。wait()
notify()
但是同步块只是奖牌的一方面:如果您实际上从非同步上下文访问同步上下文中的对象,即使在同步块中,该对象仍然不会同步,因为它拥有自己的副本缓存中的对象。我在这里写过这个问题:https ://stackoverflow.com/a/21462631和当锁持有一个非最终对象时,对象的引用仍然可以被另一个线程更改吗?
此外,我确信 x 级缓存是造成大多数不可重现的运行时错误的原因。这是因为开发人员通常不会学习低级的东西,例如 CPU 的工作方式或内存层次结构如何影响应用程序的运行:http ://en.wikipedia.org/wiki/Memory_hierarchy
为什么编程类不首先从内存层次结构和 CPU 架构开始,这仍然是一个谜。“Hello world”在这里无济于事。;)
线程等待监控对象(同步块使用的对象),单个线程全程可以有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**
}
}
根据文档:
当前线程必须拥有该对象的监视器。线程释放此监视器的所有权。
wait()
方法只是意味着它释放对象上的锁。因此对象将仅在同步块/方法中被锁定。如果线程在同步块之外意味着它没有被锁定,如果它没有被锁定,那么你会在对象上释放什么?
直接来自这个java oracle 教程:
当线程调用 d.wait 时,它必须拥有 d 的内在锁——否则会引发错误。在同步方法中调用 wait 是获取内在锁的一种简单方法。
当你调用notify()
一个对象t
时,Java 会通知一个特定的t.wait()
方法。但是,Java 如何搜索和通知特定wait
方法。
Java 只查看被对象锁定的同步代码块t
。Java 无法搜索整个代码来通知特定的t.wait()
.