解决方案
但是(如果我确实正确理解了这些方法之间的区别),始终只选择一个线程进行进一步的监视器采集。
这是不正确的。 o.notifyAll()
醒来 全部 被阻塞的线程数 o.wait()
来电。线程只允许从以下位置返回 o.wait()
一个接一个,但他们每个 将要 轮到他们了。
简而言之,这取决于您的线程等待通知的原因。您想告诉其中一个等待线程发生了什么事,还是想同时告诉所有线程?
在某些情况下,一旦等待完成,所有等待线程都可以采取有用的操作。一个例子是一组线程等待某个任务完成;一旦任务完成,所有等待线程就可以继续其业务。在这种情况下你会使用 通知全部() 同时唤醒所有等待线程。
另一种情况,例如互斥锁定,只有一个等待线程在收到通知后可以做一些有用的事情(在这种情况下获取锁)。在这种情况下,您宁愿使用 通知(). 。正确实施后,您 可以 使用 通知全部() 在这种情况下也是如此,但是您会不必要地唤醒无论如何都无法执行任何操作的线程。
在许多情况下,等待条件的代码将被编写为循环:
synchronized(o) {
while (! IsConditionTrue()) {
o.wait();
}
DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}
这样,如果一个 o.notifyAll()
调用唤醒多个等待线程,并且第一个从 o.wait()
make 使条件处于 false 状态,然后被唤醒的其他线程将返回等待状态。
其他提示
清楚地, 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)即将进入synchronized方法(上面Y点),但是生产者P1在缓冲区中放入了一个对象,随后调用 notify
. 。唯一等待的线程是 C1,因此它被唤醒,现在尝试重新获取 X 点(上图)处的对象锁。
现在C1和C2正在尝试获取同步锁。其中一个(不确定地)被选择并进入方法,另一个被阻塞(不是等待 - 而是阻塞,试图获取方法上的锁)。假设C2 首先获得锁。C1 仍然处于阻塞状态(尝试获取 X 处的锁)。C2 完成该方法并释放锁。现在,C1 获取了锁。你猜怎么着,幸运的是我们有一个 while
循环,因为 C1 执行循环检查(保护)并被阻止从缓冲区中删除不存在的元素(C2 已经得到了它!)。如果我们没有 while
, ,我们会得到一个 IndexArrayOutOfBoundsException
因为 C1 试图从缓冲区中删除第一个元素!
现在,
好吧,现在为什么我们需要notifyAll?
在上面的生产者/消费者示例中,看起来我们可以逃脱 notify
. 。看起来是这样,因为我们可以证明,守卫在 等待 生产者和消费者的循环是互斥的。也就是说,看起来我们不能让线程在等待 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获得锁,将一个字符放入缓冲区,调用notify,退出方法
第7步:
- P2的通知唤醒P3(记住任何线程都可以被唤醒)
- P3 检查等待循环条件,缓冲区中已经有一个字符,因此等待。
- 不再需要调用通知的线程,并且三个线程永久挂起!
解决方案:代替 notify
和 notifyAll
在生产者/消费者代码中(上面)。
有用的区别:
使用 通知() 如果所有等待线程都可以互换(它们唤醒的顺序并不重要),或者如果您只有一个等待线程。一个常见的示例是用于执行队列中的作业的线程池 - 当添加作业时,会通知其中一个线程唤醒,执行下一个作业并返回睡眠状态。
使用 通知全部() 对于其他情况,等待线程可能有不同的目的并且应该能够并发运行。一个示例是对共享资源的维护操作,其中多个线程在访问资源之前等待操作完成。
我认为这取决于资源的生产和消耗方式。如果同时有 5 个工作对象可用,并且您有 5 个使用者对象,则使用 notifyAll() 唤醒所有线程是有意义的,这样每个线程都可以处理 1 个工作对象。
如果只有一个可用的工作对象,那么唤醒所有消费者对象来争夺该一个对象有什么意义呢?第一个检查可用工作的线程将得到它,所有其他线程将检查并发现它们没有任何事情可做。
我找到了一个 这里有很好的解释. 。简而言之:
Notify()方法通常用于 资源池, ,如果有任意数量的“消费者”或“工人”获得资源,但是当将资源添加到池中时,只有一个等待的消费者或工人可以处理它。在大多数其他情况下,Notifyall()方法实际使用。严格来说,需要将可以允许多个服务员继续前进的状况通知服务员。但这通常很难知道。因此,作为一般规则, 如果您没有特定的逻辑来使用notify(),则可能应该使用notifyall(),因为通常很难确切知道哪些线程会在特定对象上等待。
请注意,使用并发实用程序,您还可以选择 signal()
和 signalAll()
因为这些方法在那里被调用。所以这个问题仍然有效,即使 java.util.concurrent
.
道格·李 (Doug Lea) 在他的文章中提出了一个有趣的观点 名著: :如果一个 notify()
和 Thread.interrupt()
同时发生,通知实际上可能会丢失。如果这可能发生并产生巨大影响 notifyAll()
是一个更安全的选择,即使您付出了开销的代价(大多数时候唤醒太多线程)。
Java 大师 Joshua Bloch 本人在《Effective Java》第二版中说道:
“第 69 项:更喜欢并发实用程序来等待和通知”。
这是一个例子。运行。然后将其中一个notifyAll()更改为notify(),看看会发生什么。
ProducerConsumer示例类
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) { }
}
}
}
简短的摘要:
总是更喜欢 通知全部() 超过 通知() 除非您有一个大规模并行应用程序,其中大量线程都执行相同的操作。
解释:
通知() ...]醒来一个线程。因为 通知() 不允许您指定醒来的线程,它仅在大量并行应用程序中有用,即具有大量线程的程序,都做类似的琐事。在这样的应用程序中,您不关心哪个线程被唤醒。
来源: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
比较 通知() 和 通知全部() 在上述情况下:大规模并行应用程序,其中线程执行相同的操作。如果你打电话 通知全部() 在这种情况下, 通知全部() 会引起醒来(即大量线程的调度),其中许多线程是不必要的(因为只有一个线程可以实际继续,即将被授予对象监视器的线程 等待(), 通知(), , 或者 通知全部() 被调用),因此浪费了计算资源。
因此,如果您没有一个应用程序,其中大量线程同时执行相同的操作,则更喜欢 通知全部() 超过 通知(). 。为什么?因为,正如其他用户已经在本论坛中回答的那样, 通知()
唤醒正在该对象的监视器上等待的单个线程。...]选择是 随意的 并由实施酌情发生。
来源:Java SE8 API(https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify--)
想象一下,您有一个生产者消费者应用程序,其中消费者已准备就绪(即 等待() ing)来消费,生产者已经准备好(即 等待() ing)来生产,并且项目队列(要生产/消费)为空。在这种情况下, 通知() 可能只会唤醒消费者,而不会唤醒生产者,因为唤醒谁的选择是 随意的. 。尽管生产者和消费者分别准备好生产和消费,但生产者消费者循环不会取得任何进展。相反,消费者被唤醒(即离开 等待() status),不会因为队列为空而将其从队列中取出,并且 通知() 另一个消费者继续。
相比之下, 通知全部() 唤醒了生产者和消费者。安排谁的选择取决于调度者。当然,根据调度程序的实现,调度程序也可能只调度消费者(例如如果您为消费者线程分配非常高的优先级)。然而,这里的假设是,调度程序仅调度消费者的危险低于 JVM 仅唤醒消费者的危险,因为任何合理实现的调度程序都不会仅仅使消费者 随意的 决定。相反,大多数调度程序实现至少会做出一些努力来防止饥饿。
我很惊讶没有人提到臭名昭著的“丢失唤醒”问题(谷歌一下)。
基本上:
- 如果有多个线程在相同的条件下等待,
- 多个线程可以使您从状态 A 转换到状态 B,并且,
- 可以使您从状态 B 转换到状态 A 的多个线程(通常与 1 中的线程相同),并且,
- 从状态 A 转换到 B 应该通知 1 中的线程。
那么你应该使用notifyAll,除非你有可证明的保证不可能丢失唤醒。
一个常见的示例是并发 FIFO 队列,其中:多个队列(1.和 3.上面的)可以将您的队列从空的多个dequeer转变为空(2。上面)可以等待条件“队列不是空的” - >非空的应通知Dequeuers
您可以轻松编写交错操作,其中从空队列开始,2 个入队器和 2 个出队器进行交互,1 个入队器将保持睡眠状态。
这是一个可以与死锁问题相媲美的问题。
我希望这能消除一些疑虑。
通知() :notify()方法醒来一个线程,等待锁定(该锁上在该锁上称为()的第一个线程)。
通知全部() :notifyAll()方法唤醒所有等待锁的线程;JVM从等待锁定的线程列表中选择一个线程之一,并唤醒线程向上。
单线程的情况下 等待锁时,notify()和notifyAll()没有显着区别。然而,当有多个线程等待锁时,在notify()和notifyAll()中,具体被唤醒的线程是 在JVM的控制下 并且您无法以编程方式控制唤醒特定线程。
乍一看,只调用notify()来唤醒一个线程似乎是个好主意;唤醒所有线程似乎没有必要。然而,问题是 notify() 是唤醒的线程可能不是合适的线程 被唤醒(线程可能正在等待某些其他条件,或者该线程仍然不满足条件等)。 在这种情况下, ,notify() 可能会丢失,并且其他线程都不会唤醒,这可能会导致某种死锁(通知丢失,所有其他线程都永远等待通知)。
为了避免这个问题, ,当有多个线程等待锁(或多个等待完成的条件)时,最好调用notifyAll()。notifyAll()方法会唤醒所有线程,因此效率不是很高。然而,这种性能损失在现实世界的应用中可以忽略不计。
notify()
将唤醒一个线程,同时 notifyAll()
将会唤醒所有人。据我所知,没有中间立场。但如果你不确定什么 notify()
会对你的线程做,使用 notifyAll()
. 。每次都像魅力一样发挥作用。
据我所知,上述所有答案都是正确的,所以我要告诉你一些其他的事情。对于生产代码,您确实应该使用 java.util.concurrent 中的类。在 Java 并发领域,他们几乎无所不能。
这是一个更简单的解释:
您是正确的,无论您使用notify()还是notifyAll(),直接结果都是另一个线程将获取监视器并开始执行。(假设某些线程实际上在该对象的 wait() 上被阻塞,其他不相关的线程不会占用所有可用核心等)影响会稍后出现。
假设线程 A、B 和 C 正在等待该对象,并且线程 A 获得了监视器。区别在于 A 释放监视器后会发生什么。如果使用了notify(),那么B和C仍然阻塞在wait()中:他们不是在等待监视器,而是在等待通知。当A释放监视器时,B和C仍然坐在那里,等待notify()。
如果您使用了notifyAll(),那么B和C都已经超越了“等待通知”状态,并且都在等待获取监视器。当 A 释放监视器时,B 或 C 将获取它(假设没有其他线程竞争该监视器)并开始执行。
线程有三种状态。
- WAIT - 线程未使用任何 CPU 周期
- BLOCKED - 线程在尝试获取监视器时被阻止。它可能仍在使用 CPU 周期
- RUNNING - 线程正在运行。
现在,当调用notify()时,JVM会选择一个线程并将它们移至BLOCKED状态,然后移至RUNNING状态,因为没有对监视器对象的竞争。
当调用notifyAll()时,JVM会选择所有线程并将它们全部移至BLOCKED状态。所有这些线程都会按照优先级获得该对象的锁。能够先获得监视器的线程将能够首先进入RUNNING状态,依此类推。
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
循环条件等。
取自 博客 关于有效的 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”的评论) 接受的答案 和爪哇 文档):
- 通知() :JVM 唤醒该对象上等待的线程之一。线程选择是任意的,没有公平性。所以同一个线程可以被一次又一次地唤醒。因此系统的状态发生了变化,但没有取得真正的进展。从而创建了一个 活锁.
- 通知全部():JVM 唤醒所有线程,然后所有线程争夺该对象上的锁。现在,CPU 调度程序选择一个获取该对象锁的线程。这个选择过程会比 JVM 选择要好得多。从而保证活性。
看看@xagyg 发布的代码。
假设两个不同的线程正在等待两个不同的条件:
这 第一个线程 正在等待 buf.size() != MAX_SIZE
, ,以及 第二个线程 正在等待 buf.size() != 0
.
假设在某个时刻 buf.size()
不等于0. 。JVM 调用 notify()
代替 notifyAll()
, ,并且第一个线程被通知(而不是第二个线程)。
第一个线程被唤醒,检查 buf.size()
这可能会返回 MAX_SIZE
, ,然后返回等待。第二个线程没有被唤醒,继续等待,不调用 get()
.
notify()
唤醒第一个调用的线程 wait()
在同一个物体上。
notifyAll()
唤醒所有调用的线程 wait()
在同一个物体上。
优先级最高的线程将首先运行。
我想提一下《Java并发实践》中解释的内容:
第一点,是Notify还是NotifyAll?
It will be NotifyAll, and reason is that it will save from signall hijacking.
如果两个线程A和B在相同条件队列的不同条件谓词上等待并称为通知,则最大为JVM,直接JVM将通知。
现在,如果通知是针对线程A和JVM通知线B的,则线程B将醒来,并看到此通知没有用,因此它将再次等待。线程A永远不会知道这个错过的信号,有人劫持了通知。
因此,呼叫NotifyAll将解决此问题,但是它将再次具有性能影响,因为它将通知所有线程,并且所有线程都将争夺相同的锁定,并且将涉及上下文开关,从而加载CPU。但是,只有在行为正确的情况下,如果行为本身不正确,我们才应该关心绩效。
这个问题可以通过使用jdk 5中提供的显式锁定Lock的Condition对象来解决,因为它为每个条件谓词提供了不同的等待。在这里它会正常运行并且不会出现性能问题,因为它将调用信号并确保只有一个线程正在等待该条件
notify只会通知一个处于等待状态的线程,而notify all会通知所有处于等待状态的线程,现在所有被通知的线程和所有被阻塞的线程都有资格获得锁,其中只有一个会获得锁,并且所有其他人(包括之前处于等待状态的人)将处于阻塞状态。
notify()
- 从对象的等待集中选择一个随机线程并将其放入 BLOCKED
状态。该对象的等待集中的其余线程仍在 WAITING
状态。
notifyAll()
- 将所有线程从对象的等待集中移动到 BLOCKED
状态。使用后 notifyAll()
, ,共享对象的等待集中没有剩余线程,因为它们现在都在 BLOCKED
状态而不是在 WAITING
状态。
BLOCKED
- 锁定获取被阻止。WAITING
- 等待通知(或阻止连接完成)。
总结上面精彩的详细解释,用我能想到的最简单的方式来说,这是由于 JVM 内置监视器的限制,1)是在整个同步单元(块或对象)上获取的,2)不区分正在等待/通知/关于的特定条件。
这意味着如果多个线程正在等待不同的条件并且使用了notify(),则选定的线程可能不是在新满足的条件上取得进展的线程 - 导致该线程(以及其他当前仍在等待的线程能够满足条件等..)无法取得进展,最终饥饿或程序挂起。
相反,notifyAll() 使所有等待线程最终重新获取锁并检查各自的条件,从而最终取得进展。
因此,只有当任何等待线程被保证允许在选择它时取得进展时,才可以安全地使用notify(),当同一监视器中的所有线程仅检查一个相同的条件时,通常会满足这一点 - 这是一种相当罕见的情况现实世界应用中的案例。
当您调用“对象”的 wait() 时(期望获得对象锁),intern 这将释放该对象上的锁并帮助其他线程锁定该“对象”,在这种情况下将会出现超过 1 个线程等待“资源/对象”(考虑到其他线程也在同一上述对象上发出等待,并且向下将有一个线程填充资源/对象并调用notify/notifyAll)。
在这里,当您发出同一个对象的通知(来自进程/代码的同一/另一侧)时,这将释放一个阻塞且等待的单个线程(不是所有等待线程——这个释放的线程将由 JVM 线程选取)调度器以及对象上的所有锁获取过程与常规相同)。
如果只有一个线程将共享/处理该对象,则可以在等待通知实现中单独使用 notify() 方法。
如果您遇到多个线程根据您的业务逻辑读取和写入资源/对象的情况,那么您应该使用notifyAll()
现在我正在研究当我们对一个对象发出notify()时jvm到底是如何识别和中断等待线程的...
虽然上面有一些可靠的答案,但我对我读到的困惑和误解的数量感到惊讶。这可能证明了这样一种想法:人们应该尽可能多地使用 java.util.concurrent,而不是尝试编写自己的损坏的并发代码。回到问题:总而言之,今天的最佳实践是由于丢失唤醒问题而在所有情况下避免使用notify()。任何不理解这一点的人都不应被允许编写关键任务并发代码。如果您担心羊群问题,一次唤醒一个线程的一种安全方法是:1.为等待线程建立显式等待队列;2.让队列中的每个线程等待其前驱线程;3.让每个线程在完成后调用notifyAll()。或者您可以使用 Java.util.concurrent.*,它已经实现了这一点。
唤醒一切在这里并没有多大意义。wait notification和notifyall,所有这些都放在拥有该对象的监视器之后。如果一个线程处于等待阶段并且调用了notify,则该线程将占用该锁,并且此时没有其他线程可以占用该锁。所以根本无法发生并发访问。据我所知,只有在锁定对象后才能对 wait notification 和 notifyall 进行任何调用。如果我错了请纠正我。