在Java中,当我们需要调用wait/notify/notifyAll时,我们需要访问对象监视器(通过同步方法或通过同步块)。所以我的问题是为什么java没有去同步等待/通知方法,消除从同步块或方法调用这些方法的限制。
如果它们被声明为同步的,它将自动获得监视器访问权。
对于notify和notifyAll,你的想法的问题是,当你通知你也有其他的东西,你通常在同一个同步块中做。让notify方法synchronized不会给你带来任何东西,你仍然需要block。同样,wait必须在同步块或方法中才有用,例如在自旋锁中,测试无论如何都必须同步。因此,锁的粒度与您建议的完全错误。
这里有一个例子,这是你在Java中可以拥有的最简单的队列实现:
public class MyQueue<T> {
private List<T> list = new ArrayList<T>();
public T take() throws InterruptedException {
synchronized(list) {
while (list.size() == 0) {
list.wait();
}
return list.remove(0);
}
}
public void put(T object) {
synchronized(list) {
list.add(object);
list.notify();
}
}
}
所以你可以有生产者线程添加东西到队列和消费者线程取出东西。当一个线程从队列中取出一些东西时,它需要在同步块中检查列表中是否有东西,一旦它得到通知,它需要重新获取锁并确保列表中仍然有东西(因为其他一些消费者线程可能已经介入并抓取了它)。还有"虚假的觉醒";现象:你不能依赖于被唤醒作为某事发生的充分证据,你需要检查你等待的条件是否为真,而这需要在synchronized块中完成。
在这两种情况下,需要在持有锁的情况下对等待进行检查,以便当代码根据这些检查采取操作时,它知道这些结果当前是有效的。
(如果您的用例没有像上面描述的那样需要更改的状态,那么synchronized可能是错误的工具。使用其他方法,例如CountdownLatch,可能会给您一个更简单的解决方案。
好问题。我认为JDK7 Object实现中的注释说明了这一点(强调我的):
此方法导致当前线程(称为
T
)放置在此对象的等待集中,然后放弃任何以及该对象上的所有同步声明。…然后将线程
T
从等待集中移除通常的方式是与其他线程进行正确的同步上对象;一旦它获得了对对象的控制,所有的对象上的同步声明恢复到当前状态ante -即到wait
发生时的情况方法被调用。线程T
从调用wait
方法。因此,在从wait
方法,对象和线程的同步状态T
与wait
方法时完全相同调用。
所以我认为第一点要注意的是,wait()
不会返回,直到调用者完成等待(显然)。这意味着如果wait()
本身是同步的,那么调用者将继续持有对象上的锁,并且没有其他人能够持有wait()
或notify()
。
现在很明显,wait()
在幕后做了一些棘手的事情,迫使调用者失去对对象锁的所有权,但是如果wait()
本身是同步的,也许这个技巧就不起作用了(或者会更加难以实现)。
第二点是,如果多个线程正在等待一个对象,当使用notify()
唤醒它们中的一个时,使用标准争用方法只允许一个线程在对象上同步,并且wait()
应该将调用者的同步声明恢复到调用wait()
之前的确切状态。在我看来,要求调用者在调用wait()
之前持有锁简化了这一点,因为它消除了检查调用者是否应该或不应该在wait()
返回后继续持有锁的需要。合约规定调用者必须继续持有锁,因此简化了一些实现。
或者这样做只是为了避免出现这样的逻辑悖论:"如果wait()
和notify()
都是同步的,并且wait()
直到notify()
被调用才返回,那么它们怎么可能被成功使用呢?"
这些都是我的想法。
我的猜测是synchronized
块是必需的原因是使用wait()
或notify()
作为synchronized
块中的唯一动作几乎总是一个bug。
Findbugs甚至对此有一个警告,它称之为"裸通知"。
在我读过和写过的所有无bug的代码中,它们都在一个更大的同步块中使用wait/notify
,涉及其他条件的读写
synchronized(lock)
update condition
lock.notify()
synchronized(lock)
while( condition not met)
lock.wait()
如果wait/notify
本身是synchronized
,则不会对所有正确的代码造成损害(可能会有较小的性能损失);它也不会对所有正确的代码都有任何好处。
然而,它会允许和鼓励更多的错误代码。
对多线程更有经验的人应该可以随意介入,但是我相信这会消除同步块的多功能性。使用它们的目的是在作为被监视的资源/信号量的特定对象上进行同步。然后使用Wait/notify方法来控制同步块中的执行流。
注意,同步方法是在方法(或静态方法的类)期间在this
上同步的简写。对wait/notify方法本身进行同步将使它们不再作为线程之间的停止/运行信号使用。
同步的等待通知模型要求您在继续执行任何工作之前首先获取对象上的监视器。它不同于同步块使用的互斥模型。
等待通知或相互合作模型通常用于生产者-消费者场景,其中一个线程产生由另一个线程消费的事件。编写良好的实现将努力避免消费者饥饿或生产者用太多事件超越消费者的情况。为了避免这种情况,您可以使用wait-notify协议,其中
- 消费者
wait
s用于生产者生成事件。 - 生产者生成事件,
notifies
为消费者,然后通常进入睡眠状态,直到消费者生成notified
。 - 当消费者被通知一个事件时,它会醒来,处理这个事件,并且
notifies
告诉生产者它已经完成了这个事件的处理。
在这种情况下,您可能有许多生产者和消费者。通过互斥模型获取监视器,在wait
、notify
或notifyAll
上必然会破坏该模型,因为生产者和消费者没有显式地执行等待。底层线程将出现在监视器的等待集(由等待通知模型使用)或条目集(由互斥模型使用)中。调用notify
或notifyAll
表示线程将从等待集移动到监视器的入口集(在多个线程之间可能存在对监视器的争用,而不仅仅是最近通知的线程)。
现在,当您希望使用互斥模型自动获取wait
、notify
和notifyAll
上的监视器时,这通常表明您不需要使用等待通知模型。这是通过推断得出的——只有在一个线程中做了一些工作之后,即在状态发生变化时,你才会向其他线程发出信号。如果您自动获取监视器并调用notify
或notifyAll
,那么您只是将线程从等待集移动到条目集,程序中没有任何中间状态,这意味着转换是不必要的。很明显,JVM的作者意识到了这一点,并没有将这些方法声明为synchronized。
您可以在Bill Venner的书- Inside the Java Virtual Machine中阅读更多关于监视器的等待集和输入集的信息。
我认为wait
没有synchronized
可以在某些情况下工作得很好。但是它不能用于没有竞争条件的复杂场景,可能会出现"虚假唤醒"。
代码适用于队列。
// producer
give(element){
list.add(element)
lock.notify()
}
// consumer
take(){
obj = null;
while(obj == null)
lock.wait()
obj = list.remove(0) // ignore error indexoutofrange
return obj
}
这段代码没有解释共享数据的状态,它将忽略最后一个元素,并且可能无法在多线程条件下工作。如果没有竞态条件,1
和2
中的列表状态可能完全不同。
// consumer
take(){
while(list.isEmpty()) // 1
lock.wait()
return list.remove(0) // 2
}
现在,让它变得更复杂和明显。
指令执行
-
give(element) lock.notify()->take() lock.wait() resurrected->take() list.remove(0)->rollback(element)
-
give(element) lock.notify()->take() lock.wait() resurrected->rollback(element)->take() list.remove(0)
出现"虚假唤醒",也使代码不可预测。
// producer
give(element){
list.add(element)
lock.notify()
}
rollback(element){
list.remove(element)
}
// business code
produce(element){
try{
give(element)
}catch(Exception e){
rollback(element) // or happen in another thread
}
}
// consumer
take(){
obj = null;
while(obj == null)
lock.wait()
obj = list.remove(0) // ignore error indexoutofrange
return obj
}
Chris Smith的参考资料