【问题标题】:Java: notify() vs. notifyAll() all over againJava:再次通知()与通知所有()
【发布时间】:2010-09-07 09:54:00
【问题描述】:

如果有人在 Google 上搜索“notify()notifyAll() 之间的差异”,则会弹出很多解释(撇开 javadoc 段落)。这一切都归结为被唤醒的等待线程的数量:notify() 中的一个,notifyAll() 中的所有线程。

但是(如果我确实理解这些方法之间的区别的话),总是只选择一个线程来进一步获取监视器;在第一种情况下,由 VM 选择,在第二种情况下,由系统线程调度程序选择。程序员不知道它们的确切选择过程(在一般情况下)。

那么notify()notifyAll() 之间的有用 区别是什么?我错过了什么吗?

【问题讨论】:

  • 用于并发的有用库在并发库中。我建议在几乎所有情况下这些都是更好的选择。 Concurency 库早于 Java 5.0(它们是在 2004 年作为标准添加的)
  • 我不同意彼得的观点。并发库是用Java实现的,每次调用lock()、unlock()等都会执行很多Java代码。你可以用并发库而不是旧的synchronized来打自己的脚,除了某些相当罕见的用例。
  • 关键的误解似乎是这样的:...总是选择一个线程进行进一步的监视器采集;在第一种情况下是由 VM 选择的,在第二种情况下是由系统线程调度程序选择的。 这意味着它们本质上是相同的。虽然所描述的行为是正确的,但缺少的是在notifyAll() 的情况下,_第一个线程之后的其他线程保持清醒并将一个接一个地获取监视器。在notify 的情况下,甚至没有其他线程被唤醒。所以在功能上它们是非常不同的!
  • 1) 如果有很多线程正在等待一个对象,并且 notify() 只在该对象上调用一次。除了等待线程之一,其余线程永远等待吗? 2) 如果使用 notify(),则只有一个等待线程开始执行。如果使用 notifyall() 通知所有等待的线程,但只有其中一个开始执行,那么这里的 notifyall() 有什么用?
  • @ChetanGowda 通知所有线程与仅通知一个任意线程实际上有显着差异,直到那个看似微妙但重要的差异引起我们的注意。当您仅 notify() 1 个线程时,所有其他线程都将在等待状态,直到它收到明确的通知/信号。通知所有人,所有线程将一个接一个地以某种顺序执行和完成,没有任何进一步的通知 - 这里我们应该说线程是blocked而不是waiting。当blocked它的执行被暂时挂起,直到另一个线程在sync 块内。

标签: java multithreading


【解决方案1】:

显然,notify 唤醒(任何)等待集中的一个线程,notifyAll 唤醒等待集中的所有线程。下面的讨论应该可以消除任何疑问。 notifyAll 大部分时间都应该使用。如果您不确定要使用哪个,请使用notifyAll。请参阅下面的说明。

仔细阅读并理解。如果您有任何问题,请给我发送电子邮件。

查看生产者/消费者(假设是一个具有两个方法的 ProducerConsumer 类)。它被破坏了(因为它使用notify) - 是的,它可能工作 - 即使在大多数情况下,但它也可能导致死锁 - 我们将看到原因:

public synchronized void put(Object o) {
    while (buf.size()==MAX_SIZE) {
        wait(); // called if the buffer is full (try/catch removed for brevity)
    }
    buf.add(o);
    notify(); // called in case there are any getters or putters waiting
}

public synchronized Object get() {
    // Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
    while (buf.size()==0) {
        wait(); // called if the buffer is empty (try/catch removed for brevity)
        // X: this is where C1 tries to re-acquire the lock (see below)
    }
    Object o = buf.remove(0);
    notify(); // called if there are any getters or putters waiting
    return o;
}

首先,

为什么我们需要一个围绕等待的while循环?

如果遇到这种情况,我们需要一个while 循环:

消费者 1 (C1) 进入同步块并且缓冲区为空,因此 C1 被放入等待集(通过wait 调用)。消费者 2(C2)即将进入同步方法(在上面的 Y 点),但生产者 P1 将一个对象放入缓冲区,随后调用notify。唯一等待的线程是 C1,因此它被唤醒,现在尝试在 X 点(上图)重新获取对象锁。

现在 C1 和 C2 正在尝试获取同步锁。其中一个(非确定性地)被选择并进入方法,另一个被阻塞(不等待 - 但被阻塞,试图获取方法上的锁)。假设 C2 首先获得锁。 C1 仍处于阻塞状态(试图在 X 处获取锁)。 C2 完成该方法并释放锁。现在,C1 获得了锁。猜猜看,幸运的是我们有一个while 循环,因为,C1 执行循环检查(守卫)并被阻止从缓冲区中删除一个不存在的元素(C2 已经得到它!)。如果我们没有while,我们会得到一个IndexArrayOutOfBoundsException,因为 C1 试图从缓冲区中删除第一个元素!

现在,

好的,现在我们为什么需要 notifyAll?

在上面的生产者/消费者示例中,看起来我们可以摆脱notify。看起来是这样,因为我们可以证明生产者和消费者的 wait 循环中的守卫是互斥的。也就是说,看起来我们不能在 put 方法和 get 方法中让线程等待,因为要做到这一点,那么以下条件必须是正确的:

buf.size() == 0 AND buf.size() == MAX_SIZE(假设 MAX_SIZE 不为 0)

但是,这还不够好,我们需要使用notifyAll。让我们看看为什么......

假设我们有一个大小为 1 的缓冲区(以使示例易于理解)。以下步骤导致我们陷入僵局。请注意,任何时候使用通知唤醒一个线程,它都可以由 JVM 不确定地选择 - 即任何等待的线程都可以被唤醒。另请注意,当多个线程在进入方法时阻塞(即尝试获取锁)时,获取顺序可能是不确定的。还要记住,一个线程在任何时候只能在其中一种方法中——同步方法只允许一个线程执行(即持有锁)类中的任何(同步)方法。如果发生以下事件序列 - 死锁结果:

第 1 步:
- P1 将 1 个字符放入缓冲区

第 2 步:
- P2 尝试put - 检查等待循环 - 已经是一个字符 - 等待

第 3 步:
- P3 尝试 put - 检查等待循环 - 已经是一个字符 - 等待

第 4 步:
- C1 尝试获取 1 个字符
- C2 尝试获取 1 个字符 - 阻止进入 get 方法
- C3 尝试获取 1 个字符 - 阻止进入 get 方法

第 5 步:
- C1 正在执行 get 方法 - 获取字符,调用 notify,退出方法
- notify 唤醒 P2
- 但是,C2 在 P2 之前进入方法(P2 必须重新获得锁),所以 P2 在进入 put 方法时阻塞
- C2 检查等待循环,缓冲区中没有更多字符,所以等待
- C3 在 C2 之后但在 P2 之前进入方法,检查等待循环,缓冲区中没有更多字符,所以等待

第 6 步:
- 现在:有 P3、C2 和 C3 等待!
- 最后P2获取锁,将char放入缓冲区,调用notify,退出方法

第 7 步:
- P2 的通知唤醒 P3(记住任何线程都可以被唤醒)
- P3 检查等待循环条件,缓冲区中已经有一个字符,所以等待。
- 没有更多的线程调用通知和三个线程永久暂停!

解决方案:在生产者/消费者代码(上)中将 notify 替换为 notifyAll

【讨论】:

  • finnw - P3 必须重新检查条件,因为notify 导致 P3(本例中的选定线程)从它等待的点(即在 while 循环内)继续。还有其他不会导致死锁的示例,但是,在这种情况下,使用notify 并不能保证代码无死锁。使用notifyAll 即可。
  • @marcus 非常接近。使用 notifyAll,每个线程将重新获取锁(一次一个),但请注意,在一个线程重新获取锁并执行方法(然后退出)之后......下一个线程重新获取锁,检查“while”并将返回“等待”(当然取决于条件)。因此,通知会唤醒一个线程 - 正如您正确声明的那样。 notifyAll 唤醒所有线程,每个线程一次重新获得一个锁 - 检查“while”的条件并执行该方法或再次“等待”。
  • @eran 您的描述不正确。一方面,p1 已经完成。我不会继续。
  • @codeObserver 你问:“调用 notifyAll() 会导致多个等待线程同时检查 while() 条件......因此有可能在 while 被满足之前,2线程已经超出它导致 outOfBound 异常?”不,这是不可能的,因为尽管会唤醒多个线程,但它们不能同时检查 while 条件。他们每个人都需要重新获得锁(在等待之后立即),然后才能重新进入代码部分并重新检查 while。因此,一次一个。
  • @xagyg 很好的例子。这与原始问题无关;只是为了讨论。死锁是 imo 的设计问题(如果我错了,请纠正我)。因为你有一个由 put 和 get 共享的锁。并且 JVM 不够聪明,无法在获取释放锁后调用 put,反之亦然。死锁的发生是因为 put 唤醒了另一个 put,由于 while() 而将自己放回 wait()。制作两个类(和两个锁)会起作用吗?所以 put{synchonized(get)}, get{(synchonized(put)}。换句话说,get 只会唤醒 put,而 put 只会唤醒 get。
【解决方案2】:

但是(如果我确实理解这些方法之间的区别的话),总是只选择一个线程来进一步获取监视器。

这是不正确的。 o.notifyAll() 唤醒在o.wait() 调用中阻塞的所有线程。线程只允许从o.wait() 一个接一个地返回,但每个线程轮到他们。


简单地说,这取决于您的线程等待通知的原因。你想告诉其中一个正在等待的线程发生了什么事,还是想同时告诉所有线程?

在某些情况下,一旦等待结束,所有等待的线程都可以采取有用的行动。一个例子是一组等待某个任务完成的线程;一旦任务完成,所有等待的线程都可以继续他们的业务。在这种情况下,您将使用 notifyAll() 来同时唤醒所有等待的线程。

另一种情况,例如互斥锁,只有一个等待线程在被通知后可以做一些有用的事情(在这种情况下获取锁)。在这种情况下,您宁愿使用 notify()。如果实施得当,您可以在这种情况下使用 notifyAll(),但是您会不必要地唤醒无论如何不能做任何事情的线程。


在很多情况下,等待条件的代码会写成循环:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

这样,如果 o.notifyAll() 调用唤醒了多个等待线程,并且从 o.wait() 返回的第一个使条件处于 false 状态,则被唤醒的其他线程将返回等待。

【讨论】:

  • 如果你只通知一个线程但多个线程在等待一个对象,VM如何确定通知哪一个?
  • 我不能肯定地说 Java 规范,但通常你应该避免对这些细节做出假设。不过,我认为您可以假设 VM 会以一种理智且最公平的方式来执行此操作。
  • Liedman 是严重错误的,Java 规范明确指出 notify() 不能保证是公平的。即,每次调用 notify 都可能再次唤醒同一个线程(监视器中的线程队列不是 FAIR 或 FIFO)。但是调度程序保证是公平的。这就是为什么在大多数情况下,如果你有超过 2 个线程,你应该更喜欢 notifyAll。
  • @YannTM 我完全赞成建设性的批评,但我认为你的语气有点不公平。我明确地说“不能肯定”和“我认为”。放心,你有没有在七年前写过不是 100% 正确的东西?
  • 问题是这是公认的答案,这不是个人骄傲的问题。如果您现在知道自己错了,请编辑您的答案并说出来,并指向例如xagyg 教学法和正确答案如下。
【解决方案3】:

有用的区别:

  • 如果您的所有等待线程都是可互换的(它们唤醒的顺序无关紧要),或者如果您只有一个等待线程,请使用 notify()。一个常见的例子是用于从队列执行作业的线程池——当添加作业时,通知其中一个线程唤醒、执行下一个作业并返回睡眠状态。

  • notifyAll() 用于等待线程可能有不同目的并且应该能够同时运行的其他情况。一个示例是对共享资源的维护操作,其中多个线程在访问资源之前等待操作完成。

【讨论】:

    【解决方案4】:

    我认为这取决于资源的生产和消耗方式。如果一次有 5 个工作对象可用并且您有 5 个使用者对象,则使用 notifyAll() 唤醒所有线程是有意义的,这样每个线程都可以处理 1 个工作对象。

    如果您只有一个可用的工作对象,那么唤醒所有消费者对象以竞争该对象有什么意义?第一个检查可用工作的线程将得到它,所有其他线程将检查并发现它们无事可做。

    我找到了great explanation here。简而言之:

    一般使用notify()方法 对于资源池,那里有 是任意数量的“消费者” 或占用资源的“工人”,但 当资源添加到池中时, 只有一个等待的消费者或 工人可以处理它。这 notifyAll() 方法实际用在 大多数其他情况。严格来说是 需要通知服务员 可能允许多个条件 服务员继续。但这往往 很难知道。所以作为一般 规则,如果你没有特别的 使用 notify() 的逻辑,那么你 应该可能使用 notifyAll(), 因为通常很难知道 究竟是什么线程将等待 关于特定对象以及原因。

    【讨论】:

      【解决方案5】:

      请注意,对于并发实用程序,您还可以在 signal()signalAll() 之间进行选择,因为这些方法在那里被调用。所以即使使用java.util.concurrent,这个问题仍然有效。

      Doug Lea 在他的famous book 中提出了一个有趣的观点:如果notify()Thread.interrupt() 同时发生,则通知实际上可能会丢失。如果这可能发生并产生重大影响,notifyAll() 是一个更安全的选择,即使您付出了开销的代价(大部分时间唤醒了太多线程)。

      【讨论】:

        【解决方案6】:

        这是一个例子。运行。然后将其中一个 notifyAll() 更改为 notify() 看看会发生什么。

        ProducerConsumerExample 类

        public class ProducerConsumerExample {
        
            private static boolean Even = true;
            private static boolean Odd = false;
        
            public static void main(String[] args) {
                Dropbox dropbox = new Dropbox();
                (new Thread(new Consumer(Even, dropbox))).start();
                (new Thread(new Consumer(Odd, dropbox))).start();
                (new Thread(new Producer(dropbox))).start();
            }
        }
        

        Dropbox 类

        public class Dropbox {
        
            private int number;
            private boolean empty = true;
            private boolean evenNumber = false;
        
            public synchronized int take(final boolean even) {
                while (empty || evenNumber != even) {
                    try {
                        System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                        wait();
                    } catch (InterruptedException e) { }
                }
                System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
                empty = true;
                notifyAll();
        
                return number;
            }
        
            public synchronized void put(int number) {
                while (!empty) {
                    try {
                        System.out.println("Producer is waiting ...");
                        wait();
                    } catch (InterruptedException e) { }
                }
                this.number = number;
                evenNumber = number % 2 == 0;
                System.out.format("Producer put %d.%n", number);
                empty = false;
                notifyAll();
            }
        }
        

        消费类

        import java.util.Random;
        
        public class Consumer implements Runnable {
        
            private final Dropbox dropbox;
            private final boolean even;
        
            public Consumer(boolean even, Dropbox dropbox) {
                this.even = even;
                this.dropbox = dropbox;
            }
        
            public void run() {
                Random random = new Random();
                while (true) {
                    dropbox.take(even);
                    try {
                        Thread.sleep(random.nextInt(100));
                    } catch (InterruptedException e) { }
                }
            }
        }
        

        生产者类

        import java.util.Random;
        
        public class Producer implements Runnable {
        
            private Dropbox dropbox;
        
            public Producer(Dropbox dropbox) {
                this.dropbox = dropbox;
            }
        
            public void run() {
                Random random = new Random();
                while (true) {
                    int number = random.nextInt(10);
                    try {
                        Thread.sleep(random.nextInt(100));
                        dropbox.put(number);
                    } catch (InterruptedException e) { }
                }
            }
        }
        

        【讨论】:

          【解决方案7】:

          简短总结:

          总是更喜欢 notifyAll() 而不是 notify() 除非您有一个大规模并行应用程序,其中大量线程都在做同样的事情。

          说明:

          notify() [...] 唤醒单个 线。因为 notify() 不允许你指定线程 醒来后,它只在大规模并行应用程序中有用——即 也就是说,具有大量线程的程序都在做类似的家务。 在这样的应用程序中,您不必关心唤醒哪个线程。

          来源:https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

          在上述情况下比较 notify()notifyAll():线程在做同样事情的大规模并行应用程序。如果你在这种情况下调用 notifyAll()notifyAll() 将引发大量线程的唤醒(即调度),其中许多线程是不必要的(因为仅一个线程实际上可以继续,即将被授予对象 wait()notify()notifyAll() 的监视器的线程> 被调用),因此浪费了计算资源。

          因此,如果您的应用程序没有大量线程同时执行相同的操作,则首选 notifyAll() 而不是 notify()。为什么?因为,正如其他用户已经在此论坛中回答的那样,notify()

          唤醒在该对象的监视器上等待的单个线程。 [...] 这 选择是任意的,并由 实施。

          来源:Java SE8 API (https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify--)

          假设您有一个生产者消费者应用程序,其中消费者准备好(即 wait() ing)消费,生产者准备好(即 wait() ing)生产并且项目队列(要生产/消费)是空的。在这种情况下,notify() 可能只唤醒消费者而不唤醒生产者,因为唤醒的选择是任意。尽管生产者和消费者分别准备好生产和消费,但生产者消费者周期不会取得任何进展。相反,消费者被唤醒(即离开 wait() 状态),不会从队列中取出一个项目,因为它是空的,并且 notify() s另一个消费者继续。

          相比之下,notifyAll() 唤醒了生产者和消费者。调度谁的选择取决于调度程序。当然,根据调度程序的实现,调度程序也可能只调度消费者(例如,如果您为消费者线程分配了非常高的优先级)。然而,这里的假设是调度器只调度消费者的危险低于 JVM 只唤醒消费者的危险,因为任何合理实施的调度器都不会仅仅做出任意决定。相反,大多数调度程序实现至少会做出一些努力来防止饥饿。

          【讨论】:

            【解决方案8】:

            来自 Java 大师本人在 Effective Java 第 2 版中的 Joshua Bloch:

            “第 69 条:优先使用并发实用程序来等待和通知”。

            【讨论】:

            • 为什么比来源更重要。
            • @Pacerier 说得好。我也会更有兴趣找出原因。一个可能的原因可能是对象类中的等待和通知基于隐式条件变量。因此,在标准的生产者和消费者示例中......生产者和消费者都将等待相同的条件,这可能导致死锁,正如 xagyg 在他的回答中所解释的那样。因此,更好的方法是使用docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/… 中解释的 2 个条件变量
            【解决方案9】:

            线程有三种状态。

            1. 等待 - 线程未使用任何 CPU 周期
            2. BLOCKED - 线程在尝试获取监视器时被阻塞。它可能仍在使用 CPU 周期
            3. RUNNING - 线程正在运行。

            现在,当调用 notify() 时,JVM 会选择一个线程并将它们移动到 BLOCKED 状态,从而进入 RUNNING 状态,因为没有竞争监视器对象。

            当调用 notifyAll() 时,JVM 会选择所有线程并将它们全部移动到 BLOCKED 状态。所有这些线程都会按优先级获得对象的锁。能够先获取监视器的线程将能够先进入RUNNING状态,以此类推。

            【讨论】:

            • 只是很棒的解释。
            【解决方案10】:

            这个答案是xagyg的优秀答案的图形重写和简化,包括eran的cmets。

            为什么要使用 notifyAll,即使每个产品都针对单个消费者?

            考虑生产者和消费者,简化如下。

            制作人:

            while (!empty) {
               wait() // on full
            }
            put()
            notify()
            

            消费者:

            while (empty) {
               wait() // on empty
            }
            take()
            notify()
            

            假设 2 个生产者和 2 个消费者,共享一个大小为 1 的缓冲区。下图描述了一个导致死锁的场景,如果所有线程都使用 notifyAll 可以避免这种情况>.

            每个通知都标有被唤醒的线程。

            【讨论】:

              【解决方案11】:

              我很惊讶没有人提到臭名昭著的“失去唤醒”问题(google it)。

              基本上:

              1. 如果您有多个线程在同一条件下等待,并且,
              2. 多个线程可以让您从状态 A 转换到状态 B,并且,
              3. 多个线程可以让您从状态 B 转换到状态 A(通常与 1. 中的线程相同)并且,
              4. 从状态 A 转换到 B 应该通知 1 中的线程。

              那么你应该使用 notifyAll 除非你有可证明的保证丢失唤醒是不可能的。

              一个常见的例子是并发 FIFO 队列,其中: 多个入队者(上面的 1. 和 3.)可以将您的队列从空转换为非空 多个 dequeuers (2. above) 可以等待条件“队列不为空” 空 -> 非空应通知出队

              您可以轻松编写交错操作,其中,从一个空队列开始,2 个入队者和 2 个出队者交互,1 个入队者将保持休眠状态。

              这是一个可以与死锁问题相媲美的问题。

              【讨论】:

              • 抱歉,xagyg 详细解释了。问题的名称是“丢失的唤醒”
              • @Abhay Bansal:我认为你错过了 condition.wait() 释放锁并被唤醒的线程重新获取的事实。
              【解决方案12】:

              这里有一个更简单的解释:

              您是正确的,无论您使用 notify() 还是 notifyAll(),立即的结果是恰好另一个线程将获取监视器并开始执行。 (假设某些线程实际上在此对象的 wait() 上被阻塞,其他不相关的线程并没有吸收所有可用的内核等。)影响稍后出现。

              假设线程 A、B 和 C 正在等待这个对象,而线程 A 得到了监视器。不同之处在于一旦 A 释放监视器会发生什么。如果你使用了 notify(),那么 B 和 C 仍然被阻塞在 wait() 中:它们不是在监视器上等待,而是在等待被通知。当 A 释放监视器时,B 和 C 仍会坐在那里,等待 notify()。

              如果您使用了 notifyAll(),那么 B 和 C 都已经超过了“等待通知”状态,并且都在等待获取监视器。当 A 释放监视器时,B 或 C 将获取它(假设没有其他线程竞争该监视器)并开始执行。

              【讨论】:

              • 解释得很清楚。 notify() 这种行为的结果可能会导致“Missed Signal”/“Missed Notification”导致应用程序状态出现死锁/无进展情况。P-Producer,C-Consumer P1,P2 和 C2 正在等待 C1。 C1 调用 notify() 并用于生产者,但 C2 可以被唤醒,因此 P1 和 P2 都错过了通知,将等待进一步明确的“通知”(notify() 调用)。
              【解决方案13】:

              notify() 将唤醒一个线程,而notifyAll() 将唤醒所有线程。据我所知,没有中间立场。但如果您不确定notify() 会对您的线程做什么,请使用notifyAll()。每次都像魅力一样工作。

              【讨论】:

                【解决方案14】:

                据我所知,以上所有答案都是正确的,所以我要告诉你一些其他的事情。对于生产代码,您确实应该使用 java.util.concurrent 中的类。在 java 的并发领域,他们几乎没有什么不能为你做的。

                【讨论】:

                  【解决方案15】:

                  notify() 让您编写比notifyAll() 更高效的代码。

                  考虑以下从多个并行线程执行的代码:

                  synchronized(this) {
                      while(busy) // a loop is necessary here
                          wait();
                      busy = true;
                  }
                  ...
                  synchronized(this) {
                      busy = false;
                      notifyAll();
                  }
                  

                  使用notify()可以提高效率:

                  synchronized(this) {
                      if(busy)   // replaced the loop with a condition which is evaluated only once
                          wait();
                      busy = true;
                  }
                  ...
                  synchronized(this) {
                      busy = false;
                      notify();
                  }
                  

                  如果您有大量线程,或者如果等待循环条件的评估成本很高,notify() 将明显快于notifyAll()。例如,如果您有 1000 个线程,那么将在第一个 notifyAll()、998、997 等之后唤醒和评估 999 个线程。相反,使用notify() 的方案,只会唤醒一个线程。

                  当您需要选择接下来由哪个线程执行工作时,请使用notifyAll()

                  synchronized(this) {
                      while(idx != last+1)  // wait until it's my turn
                          wait();
                  }
                  ...
                  synchronized(this) {
                      last = idx;
                      notifyAll();
                  }
                  

                  最后,重要的是要了解,在notifyAll() 的情况下,synchronized 块内已被唤醒的代码将按顺序执行,而不是一次全部执行。假设上面的例子中有三个线程在等待,第四个线程调用notifyAll()。所有三个线程都将被唤醒,但只有一个会开始执行并检查while 循环的条件。如果条件是true,它会再次调用wait(),然后第二个线程才会开始执行并检查它的while循环条件,以此类推。

                  【讨论】:

                    【解决方案16】:

                    notify() - 从对象的等待集中选择一个随机线程并将其置于BLOCKED 状态。对象等待集中的其余线程仍处于WAITING状态。

                    notifyAll() - 将所有线程从对象的等待集中移动到BLOCKED 状态。使用notifyAll() 后,共享对象的等待集中没有线程剩余,因为它们现在都处于BLOCKED 状态而不是WAITING 状态。

                    BLOCKED - 因获取锁而被阻止。 WAITING - 等待通知(或阻止加入完成)。

                    【讨论】:

                      【解决方案17】:

                      我想提一下Java并发实践中的解释:

                      第一点,Notify 还是 NotifyAll?

                      It will be NotifyAll, and reason is that it will save from signall hijacking.
                      

                      如果两个线程 A 和 B 正在等待不同的条件谓词 相同条件的队列和通知被调用,然后由JVM决定 JVM 会通知哪个线程。

                      现在如果 notify 是针对线程 A 的,而 JVM 通知线程 B,那么 线程 B 会醒来,发现这个通知没有用,所以 它会再次等待。线程 A 永远不会知道这件事 错过了信号,有人劫持了它的通知。

                      因此,调用 notifyAll 将解决此问题,但同样会 性能影响,因为它会通知所有线程并且所有线程都会 竞争同一个锁,它会涉及上下文切换,因此 负载在 CPU 上。但是我们应该只关心性能,如果它是 行为正确,如果行为本身不正确,那么 性能没有用。

                      这个问题可以通过使用 jdk 5 中提供的显式锁定 Lock 的 Condition 对象来解决,因为它为每个条件谓词提供了不同的等待。在这里它将正常运行并且不会出现性能问题,因为它会调用信号并确保只有一个线程正在等待该条件

                      【讨论】:

                        【解决方案18】:

                        取自关于 Effective Java 的 blog

                        The notifyAll method should generally be used in preference to notify. 
                        
                        If notify is used, great care must be taken to ensure liveness.
                        

                        所以,我的理解是(来自上述博客,accepted answer 和 Java docs 上的“Yann TM”评论):

                        • notify() :JVM 唤醒此对象上的一个等待线程。线程选择是任意的,没有公平性。因此可以一次又一次地唤醒同一线程。因此系统的状态发生了变化,但没有取得真正的进展。从而创建一个livelock
                        • notifyAll() :JVM 唤醒所有线程,然后所有线程争用该对象上的锁。现在,CPU 调度程序选择一个线程来获取该对象的锁定。这个选择过程会比 JVM 的选择要好得多。从而确保活性。

                        【讨论】:

                          【解决方案19】:

                          看看@xagyg 发布的代码。

                          假设两个不同的线程正在等待两个不同的条件:
                          第一个线程正在等待buf.size() != MAX_SIZE第二个线程正在等待buf.size() != 0

                          假设在某个时刻buf.size()不等于 0。 JVM 调用notify() 而不是notifyAll(),并通知第一个线程(不是第二个)。

                          第一个线程被唤醒,检查可能返回MAX_SIZEbuf.size(),然后返回等待。第二个线程没有被唤醒,继续等待,不调用get()

                          【讨论】:

                            【解决方案20】:

                            notify 将只通知一个处于等待状态的线程,而 notify all 将通知所有处于等待状态的线程,此时所有被通知的线程和所有被阻塞的线程都有资格获得锁,其中只有一个会获得锁和其他所有(包括之前处于等待状态的)都将处于阻塞状态。

                            【讨论】:

                              【解决方案21】:

                              总结上面优秀的详细解释,用我能想到的最简单的方式,这是由于JVM内置监视器的限制,它1)是在整个同步单元(块或对象)上获取的2) 不区分正在等待/通知的具体条件。

                              这意味着如果多个线程正在等待不同的条件并且使用了 notify(),则所选线程可能不是在新满足的条件上取得进展的线程 - 导致该线程(以及其他当前仍在等待的线程将能够满足条件等。)无法取得进展,最终导致饥饿或程序挂断。

                              相比之下,notifyAll() 使所有等待的线程最终重新获得锁并检查它们各自的条件,从而最终允许取得进展。

                              因此,只有当任何等待线程被选中时,才可以安全地使用 notify(),这通常在同一监视器中的所有线程只检查一个相同的条件时满足 -在现实世界的应用程序中相当罕见的情况。

                              【讨论】:

                                【解决方案22】:

                                notify() 唤醒在同一对象上调用 wait() 的第一个线程。

                                notifyAll() 唤醒在同一对象上调用wait() 的所有线程。

                                最高优先级的线程将首先运行。

                                【讨论】:

                                • notify() 的情况下,它不完全是“第一个线程”。
                                • 您无法预测 VM 会选择哪一个。只有上帝知道。
                                • 不保证谁会是第一个(不公平)
                                • 如果操作系统保证,它只会唤醒第一个线程,而且很可能不会。它实际上是由操作系统(及其调度程序)来确定要唤醒哪个线程。
                                【解决方案23】:

                                当您调用“对象”的 wait() 时(期望获得对象锁),实习生这将释放该对象上的锁并帮助其他线程锁定该“对象”,在这种情况下将有超过 1 个线程在等待“资源/对象”(考虑到其他线程也对上述同一个对象发出了等待,并且沿途会有一个线程填充资源/对象并调用 notify/notifyAll) .

                                在这里,当您发出同一对象的通知时(来自进程/代码的同一/另一端),这将释放一个阻塞和等待的单线程(不是所有等待的线程——这个释放的线程将被选中由 JVM 线程调度器和对象上的所有锁获取过程相同)。

                                如果您只有一个线程将共享/处理此对象,则可以在等待通知实现中单独使用 notify() 方法。

                                如果您处于多个线程根据您的业务逻辑读取和写入资源/对象的情况,那么您应该使用 notifyAll()

                                现在我正在查看当我们在对象上发出 notify() 时 jvm 如何准确地识别和中断等待线程...

                                【讨论】:

                                  【解决方案24】:

                                  等待队列和阻塞队列

                                  您可以假设每个锁对象关联两种队列。一种是包含等待监控锁的线程的阻塞队列,另一种是包含等待通知的线程的等待队列。 (线程调用Object.wait时会进入等待队列)。

                                  每次锁可用时,调度器从阻塞队列中选择一个线程执行。

                                  notify被调用时,等待队列中只有一个线程进入阻塞队列去争夺锁,而notifyAll将等待队列中的所有线程都放入阻塞队列中。

                                  现在你能看出区别了吗?
                                  虽然在这两种情况下都只会执行一个线程,但是使用notifyAll,其他线程仍然会执行更改(因为它们在阻塞队列中)即使它们未能争用锁。

                                  一些指导方针

                                  我基本上建议一直使用notifyAll,尽管可能会有一点性能损失。
                                  并且仅在以下情况下使用notify

                                  1. 任何被唤醒的线程都可以使程序继续进行。
                                  2. 性能很重要。

                                  例如:
                                  @xagyg 的回答给出了一个例子,notify 会导致死锁。在他的示例中,生产者和消费者都与同一个锁对象相关。所以当生产者调用notify时,可以通知生产者或消费者。但是如果生产者被唤醒,它不能让程序继续,因为缓冲区已经满了。所以会发生死锁。
                                  有两种方法可以解决:

                                  1. 按照@xagyg 的建议使用notifyALl
                                  2. 使procuder和consumer关联不同的锁对象,procuder只能唤醒consumer,consumer只能唤醒producer。在这种情况下,无论哪个消费者被唤醒,它都可以消费缓冲区并使程序继续运行。

                                  【讨论】:

                                    【解决方案25】:

                                    虽然上面有一些可靠的答案,但我对我所读到的困惑和误解的数量感到惊讶。这可能证明了应该尽可能使用 java.util.concurrent 而不是尝试编写自己的损坏并发代码的想法。

                                    回到问题:总而言之,今天的最佳实践是在所有情况下避免由于丢失唤醒问题而导致的 notify()。任何不理解这一点的人都不应被允许编写关键任务并发代码。如果您担心羊群问题,实现一次唤醒一个线程的一种安全方法是:

                                    1. 为等待线程构建显式等待队列;
                                    2. 让队列中的每个线程等待其前任;
                                    3. 完成后让每个线程调用 notifyAll()。

                                    或者你可以使用已经实现了这个的Java.util.concurrent.*。

                                    【讨论】:

                                    • 根据我的经验,等待/通知的使用通常用于队列机制中,其中线程(Runnable 实现)处理队列的内容。然后,只要队列为空,就会使用 wait()。添加信息时会调用notify()。 --> 在这种情况下,只有 1 个线程调用过 wait() ,如果你知道只有 1 个等待线程,那么使用 notifyAll() 看起来有点傻。
                                    【解决方案26】:

                                    在这里唤醒所有人并没有多大意义。 等待通知和通知,所有这些都放在拥有对象的监视器之后。如果一个线程处于等待阶段并调用了 notify,则该线程将占用锁,此时没有其他线程可以占用该锁。因此根本无法进行并发访问。据我所知,只有在锁定对象后才能进行等待通知和通知所有的调用。如果我错了,请纠正我。

                                    【讨论】:

                                      猜你喜欢
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 1970-01-01
                                      • 2015-04-24
                                      • 2013-02-28
                                      相关资源
                                      最近更新 更多