我们都知道为了调用Object.wait(),这个调用必须放在同步块中,否则抛出IllegalMonitorStateException。但是为什么要做出这样的限制呢?我知道wait()释放监视器,但为什么我们需要通过使特定块同步显式获取监视器,然后通过调用wait()释放监视器?
如果可以在同步块之外调用wait(),保留它的语义——挂起调用者线程,那么潜在的损害是什么?
我们都知道为了调用Object.wait(),这个调用必须放在同步块中,否则抛出IllegalMonitorStateException。但是为什么要做出这样的限制呢?我知道wait()释放监视器,但为什么我们需要通过使特定块同步显式获取监视器,然后通过调用wait()释放监视器?
如果可以在同步块之外调用wait(),保留它的语义——挂起调用者线程,那么潜在的损害是什么?
当前回答
只有当同时存在notify()时,wait()才有意义,因此它总是关于线程之间的通信,并且需要同步才能正确工作。有人可能会说,这应该是隐含的,但这并没有真正的帮助,原因如下:
从语义上讲,您永远不会只是等待()。你需要一些条件来满足,如果没有,你就等待,直到它满足。所以你真正要做的是
if(!condition){
wait();
}
但是这个条件是由一个单独的线程设置的,所以为了让它正确工作,你需要同步。
它还有一些错误的地方,仅仅因为你的线程退出等待并不意味着你正在寻找的条件是真的:
您可能会得到虚假的唤醒(意味着线程可以在没有收到通知的情况下从等待中唤醒),或者 可以设置条件,但是第三个线程在等待线程醒来(并重新获取监视器)时将条件再次设为false。
要处理这些情况,你真正需要的是:
synchronized(lock){
while(!condition){
lock.wait();
}
}
更好的是,完全不要乱动同步原语,而是使用java.util.concurrent包中提供的抽象。
其他回答
直接从这个Java oracle教程:
当线程调用d.wait时,它必须拥有d -的内在锁 否则抛出错误。在synchronized中调用等待 方法是一种获取内在锁的简单方法。
如果可以在同步块之外调用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来确保在isEmpty和wait之间永远不会调用notify。
不详细说明:这个同步问题是普遍存在的。正如Michael Borgwardt所指出的,wait/notify完全是关于线程之间的通信,所以您最终总是会得到类似于上面描述的竞争条件。这就是为什么强制执行“只在synchronized内部等待”规则的原因。
@Willie发布的一段链接很好地总结了这一点:
您需要绝对保证侍者和通知者就谓词的状态达成一致。服务员在稍微在进入睡眠之前的某个时间点检查谓词的状态,但它的正确性取决于谓词在进入睡眠时为真。在这两个事件之间有一段时间的漏洞,这可能会破坏程序。
生产者和消费者需要达成一致的谓词在上面的例子buffer.isEmpty()中。通过确保在同步块中执行等待和通知来解决协议。
这篇文章已经被改写为一篇文章:Java:为什么等待必须在同步块中调用
根据文档:
当前线程必须拥有该对象的监视器。线程释放 这个监视器的所有权。
Wait()方法仅仅意味着它释放对象上的锁。因此,对象只在同步块/方法中被锁定。如果线程在同步块之外意味着它没有被锁定,如果它没有被锁定那么你会释放对象上的什么?
只有当同时存在notify()时,wait()才有意义,因此它总是关于线程之间的通信,并且需要同步才能正确工作。有人可能会说,这应该是隐含的,但这并没有真正的帮助,原因如下:
从语义上讲,您永远不会只是等待()。你需要一些条件来满足,如果没有,你就等待,直到它满足。所以你真正要做的是
if(!condition){
wait();
}
但是这个条件是由一个单独的线程设置的,所以为了让它正确工作,你需要同步。
它还有一些错误的地方,仅仅因为你的线程退出等待并不意味着你正在寻找的条件是真的:
您可能会得到虚假的唤醒(意味着线程可以在没有收到通知的情况下从等待中唤醒),或者 可以设置条件,但是第三个线程在等待线程醒来(并重新获取监视器)时将条件再次设为false。
要处理这些情况,你真正需要的是:
synchronized(lock){
while(!condition){
lock.wait();
}
}
更好的是,完全不要乱动同步原语,而是使用java.util.concurrent包中提供的抽象。
我们都知道wait(), notify()和notifyAll()方法用于线程间 通信。为摆脱误信号和伪唤醒问题,等待线程 总是等待一些条件。 例如,
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
然后通知线程集wasNotified变量为true和notify。
每个线程都有自己的本地缓存,所以所有的更改都先写在那里 然后逐步提升到主存。
是否这些方法没有在同步块中调用,wasNotified变量 不会被刷新到主内存中,而是在线程的本地缓存中 因此,等待线程将继续等待信号,尽管它已通过通知重置 线程。
为了修复这些类型的问题,这些方法总是在同步块中调用 这确保当同步块开始时,那么一切将从主读取 内存,并将在退出同步块之前被刷新到主内存。
synchronized(monitor) {
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
}
谢谢,希望它能澄清。