【问题标题】:why wait/notify/notifyAll methods are not synchronized in java ?为什么等待/通知/通知所有方法在 java 中不同步?
【发布时间】:2011-10-24 14:05:07
【问题描述】:

在 Java 中,当我们需要调用 wait/notify/notifyAll 时,我们需要访问对象监视器(通过同步方法或通过同步块)。所以我的问题是为什么 java 不使用同步等待/通知方法来消除从同步块或方法中调用这些方法的限制。

如果这些被声明为同步,它将自动获取监视器访问权限。

【问题讨论】:

    标签: java multithreading synchronization synchronized


    【解决方案1】:

    对于 notify 和 notifyAll,您的想法的问题在于,当您通知您时,您通常在同一个同步块中还有其他事情。因此,使通知方法同步不会给您带来任何好处,您仍然需要该块。同样,等待必须在同步块或方法中才能有用,例如在自旋锁中,无论如何测试都必须同步。因此,对于您的建议,锁定的粒度都是错误的。

    这是一个示例,这是关于 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();
            }
        }
    }
    

    因此,您可以拥有将内容添加到队列的生产者线程和取出内容的消费者线程。当一个线程从队列中取出某些东西时,它需要在同步块中检查列表中是否存在某些东西,一旦收到通知,它需要重新获取锁并确保列表中仍然存在某些东西(因为一些其他消费者线程可能已经介入并抓住了它)。还有“虚假唤醒”现象:您不能依靠被唤醒作为发生某事的充分证据,您需要检查您正在等待的任何条件for 实际上是正确的,这需要在同步块中完成。

    在这两种情况下,围绕等待的检查都需要在持有锁的情况下进行,这样当代码根据这些检查采取行动时,它就知道这些结果当前是有效的。

    【讨论】:

    • 不确定我是否同意您总是需要同步块。考虑一个具有“下载”线程和“打印”线程以及单个锁对象的应用程序。 “Download”线程只是从一个url下载数据,完成后调用锁上的notify(),而“打印”线程在启动时只是调用锁对象上的wait(),并打印“下载完成”一旦wait() 返回。这些都没有严格要求在任一线程中添加同步块。
    • @Nathan 是的,大多数时候你的wait 在某个条件检查循环中:while(conditionVar) {... o.wait() ...} 其中conditionVar 由其他线程设置,这也会触发notify。如果第二个线程在原始线程检查了 conditionVar 但尚未开始等待时执行其业务,您将遇到严重的麻烦。如果整个 while 循环在同步块中(无竞争条件),则不会发生这种情况。
    • @aroth 由于虚假唤醒,您必须在wait() 之后重新检查条件。由于同时触及条件,这意味着wait() 必须在更大的synchronized 块内。
    • @aroth “虽然这在实践中很少发生,但应用程序必须防范它”。另一种无果唤醒的形式是notifyAll,当所有服务员都被唤醒,但只有第一个可以做任何事情;其他人必须回去等待。无论如何,因为wait 释放了锁,所以当wait 返回时不仔细检查条件是很危险的。
    • @aroth 一个更严重的问题是在进入等待之前检查条件。它必须完成,并且必须以原子方式完成(检查并在同一个锁下等待)。否则你可能会错过 notify(),并且服务员永远不会醒来。我不相信存在可以正确使用“裸等待”的用例。
    【解决方案2】:

    好问题。我认为JDK7 Object implementation 中的 cmets 对此有所了解(强调我的):

    此方法导致当前线程(称为T)放置 本身在等待这个对象的集合中然后放弃任何 以及此对象上的所有同步声明

    ...

    线程 T 然后从等待集中删除 通常的方式是与其他线程进行权限同步 目的;一旦它获得了对象的控制权,它的所有 对象上的同步声明恢复到现状 ante - 也就是说,截至wait 方法被调用线程T 然后从 调用wait 方法。因此,在从 wait方法,对象和线程的同步状态 Twait 方法使用时完全相同 调用。

    所以我认为要注意的第一点是wait() 直到调用者完成等待(显然)才返回。这意味着如果wait() 本身是同步的,那么调用者将继续持有对象上的锁,而其他人将无法使用wait()notify()

    现在显然wait() 在幕后做了一些棘手的事情,以迫使调用者无论如何都失去对对象锁定的所有权,但如果这个技巧可能不起作用(或者更难起作用),如果wait() 本身已同步。

    第二点是,如果多个线程正在等待一个对象,当notify() 用于唤醒其中一个时,标准的争用方法用于只允许一个线程在对象上同步,而@987654337 @ 应该将调用者的同步声明恢复到调用 wait() 之前的确切状态。在我看来,要求调用者在调用wait() 之前持有锁可以简化这一点,因为它不需要检查调用者是否应该在wait() 返回之后继续持有锁。合约规定调用者必须继续持有锁,因此简化了一些实现。

    或者,也许它只是为了避免出现“如果wait()notify() 都同步,并且wait() 在调用notify() 之前不会返回,那么怎么可能永远出现”能成功使用吗?”。

    无论如何,这些都是我的想法。

    【讨论】:

    • +1 好答案。仅供参考:我修复了 javadoc 引用中的代码格式。
    • 不正确。 synchronized(obj){ obj.wait(); }没有问题
    • @irreputable - 嗯?我从未说过synchronized(obj){ obj.wait(); } 有任何问题。我建议为什么需要synchronized(obj) 位。仅此而已。
    【解决方案3】:

    我的猜测是,需要 synchronized 块的原因是使用 wait()notify() 作为 synchronized 块中的唯一操作无论如何几乎总是一个错误。

    Findbugs 甚至对此有一个警告,它称之为“naked notify”。

    【讨论】:

    • 裸通知警告非常有效 - 它暗示应该使用普通的旧互斥模型。
    【解决方案4】:

    在我读过和写过的所有非错误代码中,它们都在涉及读/写其他条件的更大同步块中使用wait/notify

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

    如果wait/notify 本身就是synchronized,则不会对所有正确的代码造成任何损害(可能会造成小的性能损失);它对所有正确的代码也没有任何好处。

    但是,它会允许并鼓励更多不正确的代码。

    【讨论】:

      【解决方案5】:

      在多线程方面更有经验的人应该可以随意介入,但我相信这会消除同步块的多功能性。使用它们的目的是在充当受监视资源/信号量的特定对象上进行同步。然后使用等待/通知方法来控制同步块内的执行流程

      请注意,同步方法是在方法(或静态方法的类)期间在 this 上同步的简写。同步等待/通知方法本身将消除它们作为线程之间停止/开始信号的使用点。

      【讨论】:

        【解决方案6】:

        同步的等待通知模型要求您首先获取对象上的监视器,然后再继续执行任何工作。它不同于同步块使用的互斥模型。

        等待通知或相互合作模型通常用于生产者-消费者场景,其中一个线程产生由另一个线程消费的事件。编写良好的实现将努力避免消费者挨饿或生产者用太多事件超出消费者的情况。为避免这种情况,您将使用等待通知协议,其中

        • 消费者waits 让生产者产生一个事件。
        • 生产者产生事件,notifies 消费者,然后通常进入睡眠状态,直到消费者 notified
        • 当消费者收到事件通知时,它会醒来并处理该事件,并且notifies 生产者已完成对事件的处理。

        在这种情况下,您可能有许多生产者和消费者。通过互斥模型获取监视器,在waitnotifynotifyAll 上必然会破坏该模型,因为生产者和消费者没有显式地执行等待。底层线程将出现在监视器的等待集(由等待通知模型使用)或条目集(由互斥模型使用)中。调用notifynotifyAll 表示线程要从等待集移动到监视器的入口集(其中可能存在多个线程之间的监视器争用,而不仅仅是最近通知的线程)。

        现在,当您想使用互斥模型自动获取waitnotifynotifyAll 上的监视器时,通常表明您不需要使用等待通知模型。这是通过推理得出的——您通常只会在一个线程中完成一些工作(即状态发生变化)之后才向其他线程发出信号。如果您自动获取监视器并调用notifynotifyAll,那么您只是将线程从等待集移动到入口集,程序中没有任何中间状态,这意味着过渡是不必要的。很明显,JVM 的作者意识到了这一点,并没有将这些方法声明为同步。

        您可以在 Bill Venner 的书 - Inside the Java Virtual Machine 中阅读有关监视器的等待集和入口集的更多信息。

        【讨论】:

          【解决方案7】:

          我认为没有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()-&gt;take() lock.wait() resurrected-&gt;take() list.remove(0)-&gt;rollback(element)
          • give(element) lock.notify()-&gt;take() lock.wait() resurrected-&gt;rollback(element)-&gt;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 Smith Reference of insidevm

          【讨论】:

            猜你喜欢
            • 2015-01-26
            • 2012-03-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-11-12
            • 2017-09-16
            • 2014-01-02
            • 1970-01-01
            相关资源
            最近更新 更多