Java: notify() vs. notifyAll()

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

然而(如果我确实理解了这些方法之间的区别),只有一个线程总是被选择用于进一步的监视采集;第一种情况是VM选择的线程,第二种情况是系统线程调度程序选择的线程。程序员不知道它们的确切选择过程(在一般情况下)。

那么,notify ()notifyAll ()之间的有用的区别是什么?我遗漏了什么吗?

229972 次浏览

notify()将唤醒一个线程,而notifyAll()将唤醒所有线程。据我所知,没有中间立场。但是如果你不确定notify()会对你的线程做什么,使用notifyAll()。每次都很灵验。

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

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

我找到了一个这里有很好的解释。简而言之:

通常使用notify()方法 for 资源池,其中存在 是任意数量的“消费者” 或者是“工人”占用资源,但是 当资源被添加到池中时, 只有一个等待的消费者或 工人们可以应对。的 notifyAll()方法实际用于 大多数其他情况。严格来说,它是 要求通知服务员 条件,可以允许多个 服务员请继续。但这通常是 很难知道。所以一般来说 规则,如果你没有特殊的 使用notify()的逻辑,然后使用 应该使用notifyAll(), 因为通常很难知道 将要等待的线程

但是(如果我正确理解了这些方法之间的区别),总是只选择一个线程进行进一步的监视采集。

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


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

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

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


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

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

这样,如果o.notifyAll()调用唤醒多个等待线程,并且第一个从o.wait() make返回的线程将条件留在false状态,那么其他被唤醒的线程将返回等待。

有用的差异:

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

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

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

摘自Java大师Joshua Bloch在Effective Java第二版中的文章:

“第69项:选择并发实用程序而不是等待和通知”。

注意,在并发实用程序中,你也可以在signal()signalAll()之间进行选择,因为这些方法在那里被调用。因此,问题仍然有效,即使是java.util.concurrent

Doug Lea在他的著名的书中提出了一个有趣的观点:如果notify()Thread.interrupt()同时发生,通知实际上可能会丢失。如果这种情况发生,并有戏剧性的影响notifyAll()是一个更安全的选择,即使你付出了开销的代价(大多数时候唤醒太多的线程)。

醒来在这里没有太大意义。 Wait notify和notifyall,所有这些都放在拥有对象的监视器之后。如果一个线程处于等待阶段,并且调用了notify,那么这个线程将占用该锁,此时没有其他线程可以占用该锁。所以并发访问根本不能发生。据我所知,只有在锁定对象后才能调用wait notify和notifyall。如果我错了请指正。

这里有一个例子。运行它。然后将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) { }
}
}
}

显然,notify唤醒等待集中的(任意)一个线程,notifyAll唤醒等待集中的所有线程。下面的讨论应能消除任何疑问。notifyAll应该在大多数时候使用。如果你不确定使用哪个,那么使用notifyAll。请看下面的解释。

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

查看生产者/消费者(假设是一个具有两个方法的ProducerConsumer类)。IT IS BROKEN(因为它使用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,当C1试图从缓冲区中删除第一个元素时,我们将得到IndexArrayOutOfBoundsException !

现在,

为什么我们需要notifyAll?

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

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

然而,这还不够好,我们需要使用notifyAll。让我们看看为什么……

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

< p > 步骤1: < BR > - P1将1个字符放入缓冲区

< p > 步骤2: < BR > P2尝试put -检查等待循环-已经是一个字符-等待

< p > 步骤3: < BR > P3尝试put -检查等待循环-已经是一个字符-等待

< p > 步骤4: < BR > - C1尝试获取1个char
C2尝试在get方法
的入口获取1个字符块 - C3尝试在get方法的入口获得1个字符块

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

< p > 步骤6: < BR > -现在:有P3, C2,和C3等待!< BR > -最后P2获取锁,在缓冲区中放入一个字符,调用notify,退出方法

< p > 第七步: < BR > P2的通知唤醒了P3(记住任何线程都可以被唤醒)
P3检查等待循环条件,缓冲区中已经有一个字符,所以等待 没有更多线程调用通知和三个线程永久挂起!< / p >

解决方案:在上面的生产者/消费者代码中用notifyAll替换notify

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

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

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

看看@xagyg发布的代码。

假设两个不同的线程正在等待两个不同的条件 第一个线程等待buf.size() != MAX_SIZE,而第二个线程等待buf.size() != 0。< / p >

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

第一个线程被唤醒,检查buf.size()是否返回MAX_SIZE,然后继续等待。第二个线程没有被唤醒,继续等待并且不调用get()

我很惊讶居然没有人提到臭名昭著的“失醒”问题(谷歌it)。

基本上:

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

然后,您应该使用notifyAll,除非您有可证明的保证,丢失的唤醒是不可能的。

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

您可以很容易地编写一个交叉操作,其中从一个空队列开始,2个入队者和2个出队者交互,1个入队者保持休眠状态。

这是一个可以与死锁问题相比较的问题。

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个线程,那么999个线程将在第一个notifyAll()之后被唤醒并计算,然后是998,然后是997,等等。相反,使用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循环条件,以此类推。

这里有一个简单的解释:

您是正确的,无论使用notify()还是notifyAll(),直接结果都是恰好有另一个线程获得监视器并开始执行。(假设一些线程实际上在wait()上阻塞了这个对象,其他不相关的线程并没有占用所有可用的内核,等等。)

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

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

我想提一下《Java并发实践》中解释的内容:

第一点,是Notify还是NotifyAll?

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

如果两个线程A和B正在等待不同的条件谓词 在相同条件下调用queue和notify,然后由JVM to

现在如果notify是针对线程A的,而JVM通知线程B,那么 线程B会被唤醒,发现这个通知没有用 它将再次等待。线程A永远不会知道这个 错过了信号,有人劫持了它的通知 所以,调用notifyAll将解决这个问题,但它将再次 性能影响,因为它会通知所有线程,所有线程都会通知 竞争同一把锁,会涉及上下文切换,因此 CPU负载。但我们应该只关心表现,如果它是 行为正确,如果行为本身不正确

.性能是没有用的

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

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

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

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

相反,notifyAll()允许所有等待的线程最终重新获得锁并检查各自的条件,从而最终允许执行进程。

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

简短的总结:

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

解释:

< p > notify ()[…醒来时还是单身 线程。因为notify ()不允许你指定线程 唤醒后,它只在大规模并行应用程序中有用 是指具有大量线程的程序,它们都执行类似的任务。 在这样的应用程序中,你不关心哪个线程被唤醒

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

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

因此,如果你的应用程序中没有大量线程并发地做同样的事情,最好使用notifyAll ()而不是notify ()。为什么?因为,正如其他用户已经在这个论坛上回答的那样,notify ()

唤醒正在此对象的监视器上等待的单个线程。[…] 选项任意的,由 实现。< / p >

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

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

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

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

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

BLOCKED -阻塞锁获取。 WAITING -等待通知(或阻止连接完成)

当你调用wait()的“对象”(期望对象锁)、实习生这将释放锁,物体和帮助的其他线程锁在这个“对象”,在这种情况下,将会有超过1线程等待“资源/对象”(考虑到其他线程也发布了等待上面相同的对象,将会有一个线程的方式填补资源/对象并调用通知/ notifyAll)。

在这里,当您(从进程/代码的同一/另一端)发出同一对象的通知时,这将释放一个阻塞和等待的单个线程(不是所有等待的线程——这个释放的线程将由JVM thread Scheduler挑选,对象上的所有锁获取进程与常规进程相同)。

如果只有一个线程共享/处理这个对象,那么可以在wait-notify实现中单独使用notify()方法。

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

现在我正在寻找JVM是如何识别和打破等待线程时,我们发出通知()在一个对象…

线程有三种状态。

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

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

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

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

回到问题:总结一下,目前的最佳实践是在所有情况下避免notify(),因为会出现丢失唤醒的问题。任何不理解这一点的人都不应该被允许编写关键任务并发代码。如果你担心羊群问题,实现一次唤醒一个线程的安全方法是:

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

或者你可以使用Java.util.concurrent。*,它们已经实现了这一点。

这个答案是对xagyg的优秀答案的图形化重写和简化,包括伊兰的注释。

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

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

制作人:

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

消费者:

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

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

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

deadlock due to notify

摘自blog on Effective Java:

The notifyAll method should generally be used in preference to notify.


If notify is used, great care must be taken to ensure liveness.

所以,我的理解是(从前面提到的博客,“Yann TM”对接受的答案和Java 文档的评论):

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

等待队列和阻塞队列

您可以假设与每个锁对象关联的队列有两种类型。一个是阻塞队列,包含等待监控器锁的线程,另一个是等待队列,包含等待通知的线程。(当线程调用Object.wait时,线程将被放入等待队列)。

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

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

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

一些指导原则

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

时使用notify
  1. 任何被唤醒的线程都可以使程序继续运行。
  2. 性能很重要。
< p > 例如:
@xagyg的回答给出了一个notify将导致死锁的例子。在他的例子中,生产者和消费者都与同一个锁对象相关。因此,当生产者调用notify时,可以通知生产者或消费者。但是,如果一个生产者被唤醒,它就不能使程序继续进行,因为缓冲区已经满了。因此发生了死锁。
有两种方法来解决它:

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