如果一个谷歌用于"区别notify()
和之间notifyAll()
",那么会弹出很多解释(将javadoc段落分开).这一切都归结为等待线程被唤醒的数量:一个进入notify()
和全部进入notifyAll()
.
但是(如果我确实理解了这些方法之间的区别),总是只选择一个线程用于进一步的监视器获取; 在第一种情况下,由VM选择的一种情况,在第二种情况下由系统线程调度程序选择的一种情况.一般情况下,程序员都不知道它们的确切选择程序(在一般情况下).
那么notify()和notifyAll()之间的有用区别是什么?我错过了什么吗?
显然,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已经得到它!).如果我们没有a while
,我们会得到一个,IndexArrayOutOfBoundsException
因为C1试图从缓冲区中删除第一个元素!
现在,
好的,现在为什么我们需要notifyAll?
在上面的生产者/消费者示例中,看起来我们可以逃脱notify
.这似乎是这样,因为我们可以证明生产者和消费者的等待循环上的守卫是相互排斥的.也就是说,看起来我们不能在put
方法和方法中等待一个线程get
,因为为了那个是真的,那么以下必须是真的:
buf.size() == 0 AND buf.size() == MAX_SIZE
(假设MAX_SIZE不为0)
但是,这还不够好,我们需要使用notifyAll
.让我们看看为什么......
假设我们有一个大小为1的缓冲区(使示例易于遵循).以下步骤导致我们陷入僵局.请注意,ANYTIME线程被通知唤醒,它可以由JVM非确定性地选择 - 即可以唤醒任何等待的线程.还要注意,当多个线程在进入方法时阻塞(即尝试获取锁定)时,获取的顺序可能是不确定的.还要记住,一个线程在任何时候都只能在其中一个方法中 - 同步方法只允许一个线程在类中执行(即保持锁定)任何(同步)方法.如果发生以下事件序列 - 死锁结果:
步骤1:
- P1将1个字符放入缓冲区
第2步:
- P2尝试put
- 检查等待循环 - 已经是char - 等待
第3
步: - P3尝试put
- 检查等待循环 - 已经是char - 等待
第4步:
- C1尝试获取1个char
- C2尝试获取1个char - 在进入get
方法时阻塞
- C3尝试获取1个char - 在进入get
方法时阻塞
步骤5:
- C1正在执行get
方法 - 获取char,调用notify
,退出方法
- notify
唤醒P2
- BUT,C2在P2之前进入方法(P2必须重新获取锁定),因此P2阻止进入put
方法
- C2检查等待循环,缓冲区中没有更多的字符,所以等待
- C3在C2之后进入方法,但在P2之前,检查等待循环,缓冲区中不再有字符,所以等待
第6步:
- 现在:有P3,C2和C3等待!
- 最后P2获取锁,将char放入缓冲区,调用notify,退出方法
STEP 7:
- P2的通知唤醒P3(记住任何线程都可以被唤醒)
- P3检查等待循环条件,缓冲区中已经有一个char,所以等待.
- 没有更多的线索要求通知和三个线程永久性地暂停!
解决办法:更换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().一个示例是对共享资源的维护操作,其中多个线程在访问资源之前等待操作完成.
我认为这取决于资源的生产和消费方式.如果一次有5个工作对象并且你有5个消费者对象,那么使用notifyAll()唤醒所有线程是有意义的,这样每个人都可以处理1个工作对象.
如果您只有一个工作对象可用,那么唤醒所有消费者对象以竞争该对象的重点是什么?检查可用工作的第一个将获得它,所有其他线程将检查并发现它们无关.
我在这里找到了很好的解释.简而言之:
notify()方法通常用于资源池,其中有任意数量的"使用者"或"工作者"占用资源,但是当资源添加到池中时,只有一个等待的消费者或工人可以处理用它.notifyAll()方法实际上在大多数其他情况下使用.严格来说,需要通知服务员一个可能允许多个服务员继续进行的情况.但这通常很难知道.因此,作为一般规则,如果您没有使用notify()的特定逻辑,那么您应该使用notifyAll(),因为通常很难确切知道哪些线程将等待特定对象及其原因.
请注意,使用并发实用程序时,您还可以选择signal()
并signalAll()
在这些方法中调用这些方法.所以这个问题即使有效也仍然有效java.util.concurrent
.
Doug Lea在他的着名书中提出了一个有趣的观点:如果a notify()
和Thread.interrupt()
同时发生,通知实际上可能会丢失.如果这种情况发生并具有戏剧性的影响notifyAll()
是一种更安全的选择,即使您支付了开销的代价(大多数时间都会唤醒过多的线程).
来自Jos Java Bloch,Java Guru本人参加Effective Java第2版:
"项69:首选并发实用程序等待并通知".
这是一个例子.运行.然后将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) { } } } }
简短的摘要:
总是更喜欢notifyAll()而不是notify(),除非你有一个大规模并行的应用程序,其中大量的线程都做同样的事情.
说明:
notify() [...]唤醒单个线程.因为notify()不允许您指定被唤醒的线程,所以它仅在大规模并行应用程序中有用 - 即具有大量线程的程序,所有程序都执行类似的工作.在这样的应用程序中,您不关心哪个线程被唤醒.
来源:https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
在上述情况下将notify()与notifyAll()进行比较:一个大规模并行的应用程序,其中线程正在做同样的事情.如果在这种情况下调用notifyAll(),notifyAll()将导致大量线程的唤醒(即调度),其中许多线程不必要(因为只有一个线程可以实际进行,即将被授予的线程)监视对象wait(),notify()或notifyAll()被调用),因此浪费计算资源.
因此,如果您没有一个应用程序,其中大量线程同时执行相同的操作,则首选notifyAll()而不是notify().为什么?因为,正如其他用户已在此论坛中回答的那样,通知()
唤醒正在此对象监视器上等待的单个线程.[...]选择是任意的,由实施决定.
来源:Java SE8 API(https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify--)
想象一下,你有一个生产者消费者应用程序,消费者准备就绪(即等待())消费,生产者准备好(即等待())生产和物品队列(待生产/消费)是空的.在这种情况下,notify()可能只唤醒消费者而不会唤醒生产者,因为被唤醒的选择是任意的.尽管生产者和消费者分别准备生产和消费,但生产者消费者周期不会取得任何进展.相反,消费者被唤醒(即离开wait()状态),不会将项目从队列中取出,因为它是空的,并且notify()是另一个消费者继续.
相比之下,notifyAll()唤醒了生产者和消费者.计划的选择取决于调度程序.当然,根据调度程序的实现,调度程序可能也只安排使用者(例如,如果您为消费者线程分配了非常高的优先级).但是,这里的假设是调度程序仅调度消费者的危险低于JVM仅唤醒消费者的危险,因为任何合理实现的调度程序都不会做出任意决策.相反,大多数调度程序实现至少需要一些努力来防止饥饿.
线程有三种状态。
等待-线程未使用任何CPU周期
BLOCKED-尝试获取监视器的线程被阻塞。它可能仍在使用CPU周期
RUNNING-线程正在运行。
现在,当调用notify()时,JVM将选择一个线程并将其移至BLOCKED状态,从而移至RUNNING状态,因为没有与监视对象的竞争。
调用notifyAll()时,JVM会选择所有线程并将它们全部移到BLOCKED状态。所有这些线程将优先获得对象的锁定。首先能够获取监视器的线程将能够首先进入RUNNING状态,依此类推。
我很惊讶没有人提到臭名昭着的"失去的唤醒"问题(google it).
基本上:
如果你有多个线程在相同的条件下等待,
多个线程可以让你从状态A转换到状态B,
多个线程,可以让你从状态B转换到状态A(通常与1中的线程相同),以及
从状态A转换到B应该通知1中的线程.
那么你应该使用notifyAll,除非你有可证明的保证不可能丢失唤醒.
一个常见的例子是并发FIFO队列,其中:多个队列(上面的1.和3.)可以将队列从空转换为非空的多个队列(上面的2.)可以等待条件"队列不为空"为空 - >非空应该通知dequeuers
您可以轻松地编写一个交错操作,其中从空队列开始,2个入队者和2个出队员进行交互,1个入口者将保持休眠状态.
这个问题可以说与死锁问题相当.
我希望这会清除一些疑问.
notify():notify()方法唤醒一个等待锁的线程(在该锁上调用wait()的第一个线程).
notifyAll():notifyAll()方法唤醒等待锁定的所有线程; JVM从等待锁定的线程列表中选择一个线程并唤醒该线程.
在单个线程等待锁定的情况下, notify()和notifyAll()之间没有显着差异.但是,当有多个线程等待锁定时,在notify()和notifyAll()中,唤醒的确切线程都在JVM的控制之下,并且您无法以编程方式控制唤醒特定线程.
乍一看,似乎只需调用notify()来唤醒一个线程; 唤醒所有线程似乎没有必要.然而,notify()的问题在于唤醒的线程可能不适合被唤醒(线程可能正在等待某些其他条件,或者该线程仍然不满足条件等).在这种情况下,notify()可能会丢失,并且没有其他线程可能会唤醒,从而可能导致一种死锁(通知丢失,所有其他线程都在等待永久通知).
为避免此问题,最好在有多个线程等待锁定时调用notifyAll()(或者多个条件等待完成).notifyAll()方法唤醒所有线程,因此效率不高.但是,在实际应用中,这种性能损失可以忽略不计.