为什么wait/notify/notifyAll方法在java中不同步?

rpppsulh  于 2022-12-17  发布在  Java
关注(0)|答案(7)|浏览(122)

在Java中,无论何时我们需要调用wait/notify/notifyAll,我们都需要访问对象监视器(通过synchronized方法或synchronized块),所以我的问题是为什么Java不使用synchronized wait/notify方法,从而取消从synchronized块或方法调用这些方法的限制。
如果它们被声明为synchronized,它将自动获取监视器访问权限。

xpszyzbs

xpszyzbs1#

对于notify和notifyAll,你的想法的问题在于当你通知的时候,你通常在同一个synchronized块中还有其他的事情要做。所以让notify方法synchronized不会给你带来任何好处,你仍然需要这个块。同样的,wait必须在synchronized块或方法中才有用。比如在一个自旋锁里面,测试无论如何都要同步,所以你所建议的锁定粒度是完全错误的。
下面是一个例子,这是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块中完成.
在这两种情况下,都需要在持有锁的情况下对等待进行检查,以便代码在根据这些检查采取行动时知道这些结果当前是有效的。
(If您的用例不具有需要如上所述更改的状态,那么synchronized可能是不适合该工作的工具。使用一些其他方法,例如CountdownLatch,可能会给您提供更简单的解决方案。)

jjjwad0x

jjjwad0x2#

问得好。我认为JDK7 Object implementation中的评论对这一点有所启发(强调我的):
此方法使当前线程(称为T)将其自身置于此对象的等待集中,然后放弃此对象上的所有同步请求。
...
然后,线程T以这种通常的方式与其他线程一起从等待集中移除,以获得在该对象上同步的权限;一旦线程T获得了对对象的控制,它对对象的所有同步声明都恢复到以前的状态-即恢复到调用wait方法时的情形线程T然后从wait方法的调用返回。因此,在从wait方法返回时,对象和线程X1 M6 N1 X的同步状态与调用X1 M7 N1 X方法时的状态完全相同。
所以我想首先要注意的是wait()在调用者完成等待之前不会返回(很明显),这意味着如果wait()本身是同步的,那么调用者将继续持有对象的锁,其他人将无法使用wait()notify()
现在很明显,wait()正在幕后做一些棘手的事情,以迫使调用者无论如何都失去对对象锁的所有权,但如果wait()本身是同步的,那么这个技巧可能不会起作用(或者会明显更难起作用)。
第二点是,如果多个线程正在等待一个对象,当notify()用于唤醒其中一个线程时,标准争用方法用于只允许一个线程在该对象上同步,并且wait()应该将调用者的同步声明恢复到它们在调用wait()之前所处的确切状态。在我看来,要求调用方在调用wait()之前持有锁可以简化这一点,因为它消除了检查调用方是否应该在wait()返回后继续持有锁的需要。契约规定调用方必须继续持有锁,因此简化了一些实现。
或者,这样做只是为了避免出现逻辑悖论:“如果wait()notify()都是同步的,并且wait()直到notify()被调用时才返回,那么如何成功地使用其中的任何一个呢?"
不管怎么说,这就是我的想法。

tpgth1q7

tpgth1q73#

我的猜测是,需要synchronized块的原因是,使用wait()notify()作为synchronized块中的唯一操作几乎总是一个bug。
Findbugs甚至对此有一个警告,它称之为“裸通知”。

vtwuwzda

vtwuwzda4#

在我读过和写过的所有没有bug的代码中,所有这些代码都在一个更大的同步块中使用wait/notify,该同步块涉及其他条件的读/写

synchronized(lock)
    update condition
    lock.notify()

synchronized(lock)
    while( condition not met)
        lock.wait()

如果wait/notify本身是synchronized,则对所有正确的代码没有损害(可能是小的性能损失);对所有正确的代码也没有任何好处。
然而,它会允许和鼓励更多的不正确的代码。

jv4diomz

jv4diomz5#

有多线程经验的人可以随时介入,但我相信这会消除同步块的多功能性。使用它们的目的是在一个特定的对象上同步,该对象充当被监视的资源/信号量。然后使用wait/notify方法来控制同步块 * 内 * 的执行流。
注意synchronized方法是在this上在方法(或静态方法的类)持续时间内同步的简写,同步wait/notify方法本身将删除它们作为线程之间的停止/执行信号的使用点。

2cmtqfgy

2cmtqfgy6#

同步的wait-notify模型要求在继续执行任何工作之前,首先获取对象上的监视器。它与同步块使用的互斥模型不同。
等待-通知或相互协作模型通常用于生产者-消费者方案,其中一个线程生成由另一个线程消费的事件。编写良好的实现将努力避免消费者饥饿或生产者因事件过多而使消费者超载的方案。要避免这种情况,您将使用等待-通知协议,其中

  • 消费者wait用于生产者产生事件。
  • 生产者生产事件,消费者生产X1 M1 N1 X,然后通常进入睡眠,直到消费者生产X1 M2 N1 X。
  • 当向消费者通知事件时,消费者醒来,处理该事件,并且生产者通知它已经完成了对该事件的处理。

在这个场景中,你可能有很多生产者和消费者,通过互斥模型获取监视器,在waitnotifynotifyAll上,必然会破坏这个模型。因为生产者和消费者并不显式地执行等待。底层线程将出现在任一等待集中(由等待通知模型使用)或条目集(由互斥模型使用)。调用notifynotifyAll信号线程(s)从等待集合移动到监视器的条目集合(其中在多个线程之间可能存在对监视器的争用,而不仅仅是最近通知的一个)。
现在,当您希望使用互斥模型自动获取waitnotifynotifyAll上的监视器时,这通常表明您不需要使用等待通知模型。这是通过推理得出的-您通常只会在一个线程中完成一些工作后才向其他线程发送信号。如果您自动获取监视器并调用notifynotifyAll,则您只是将线程从等待集移动到入口集,而程序中没有任何中间状态,这意味着转换是不必要的。JVM的作者意识到了这一点,并且没有声明这些方法是同步的。
您可以在Bill Venner的书-Inside the Java Virtual Machine中阅读更多关于监视器的等待集和入口集的内容。

vdgimpew

vdgimpew7#

我认为没有synchronizedwait在某些情况下可以很好地工作。但是它不能用于没有竞态条件的复杂情况,可能会出现“伪唤醒”。
代码适用于队列。

// 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
}

这段代码没有解释共享数据的状态,它将忽略最后一个元素,并且可能在多线程条件下不起作用。如果没有争用条件,12中的这个列表可能有完全不同的状态。

// 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
}

Reference of Chris SmithReference of insidevm

相关问题