Java:再次遍历notify()与notifyAll()
如果一个Google提出" notify()和notifyAll()之间的区别",那么将会弹出很多说明(将javadoc段落分开)。最终归结为唤醒等待线程的数量:一个在notify()
中,一个在notifyAll()
中。
但是(如果我确实正确理解了这两种方法之间的区别),总是只选择一个线程来进行进一步的监视器获取;在第一种情况下,由VM选择,在第二种情况下,由系统线程调度程序选择。程序员不知道它们的确切选择过程(在一般情况下)。
然后notify()和notifyAll()的有用区别是什么?我想念什么吗?
解决方案
回答
notify()
将唤醒一个线程,而notifyAll()
将唤醒所有线程。据我所知,没有中间立场。但是如果不确定notify()
对线程有什么作用,请使用notifyAll()
。每次都像魅力一样。
回答
我认为这取决于资源的生产和消费方式。如果一次有5个工作对象可用,而我们有5个使用者对象,则可以使用notifyAll()唤醒所有线程,以便每个线程可以处理1个工作对象。
如果只有一个工作对象可用,唤醒所有消费者对象争用该对象有什么意义?第一个检查可用工作的程序将获得此权限,所有其他线程将检查并发现它们无关。
我在这里找到了很好的解释。简而言之:
The notify() method is generally used for resource pools, where there are an arbitrary number of "consumers" or "workers" that take resources, but when a resource is added to the pool, only one of the waiting consumers or workers can deal with it. The notifyAll() method is actually used in most other cases. Strictly, it is required to notify waiters of a condition that could allow multiple waiters to proceed. But this is often difficult to know. So as a general rule, if you have no particular logic for using notify(), then you should probably use notifyAll(), because it is often difficult to know exactly what threads will be waiting on a particular object and why.
回答
简而言之,这取决于线程为何等待被通知。我们是否要告诉一个正在等待的线程发生了什么,还是要同时告诉所有这些线程?
在某些情况下,等待完成后,所有等待线程都可以采取有用的措施。一个示例是一组等待某个任务完成的线程。任务完成后,所有等待的线程都可以继续其业务。在这种情况下,我们可以使用notifyAll()来同时唤醒所有正在等待的线程。
另一种情况,例如互斥锁定,只有一个等待线程在得到通知后可以做一些有用的事情(在这种情况下,获取锁定)。在这种情况下,我们宁愿使用notify()。正确实现后,在这种情况下,我们也可以使用notifyAll(),但不必要地唤醒了无法执行任何操作的线程。
回答
有用的区别:
- 如果所有等待线程都是可互换的(唤醒顺序无关紧要),或者只有一个等待线程,请使用notify()。一个常见的示例是用于执行队列中作业的线程池-添加作业后,将通知一个线程唤醒,执行下一个作业并返回睡眠状态。
- 在其他情况下,等待线程可能有不同的用途,并且应该能够并行运行,请使用notifyAll()。一个示例是对共享资源的维护操作,其中多个线程在访问该资源之前正在等待操作完成。
回答
就我所知,以上所有答案都是正确的,因此我将告诉我们其他内容。对于生产代码,我们确实应该使用java.util.concurrent中的类。在Java的并发方面,它们对我们几乎无能为力。
回答
来自Java专家Joshua Bloch的《有效Java第二版》:
"项目69:建议并发实用程序等待并通知"。
回答
请注意,使用并发实用程序,我们还可以在signal()
和signalAll()
之间进行选择,因为在那里调用了这些方法。因此,即使使用java.util.concurrent
,该问题仍然有效。
道格·李(Doug Lea)在他的著名著作中提出了一个有趣的观点:如果同时发生" notify()"和" Thread.interrupt()",则通知实际上可能丢失。如果发生这种情况并产生重大影响,即使我们付出开销(大多数情况下会消耗太多线程)的代价," notifyAll()"是一个更安全的选择。
回答
唤醒所有人在这里没有多大意义。
等待notify和notifyall,所有这些都放在拥有对象的监视器之后。如果某个线程处于等待阶段并调用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类,具有两个方法)。它已经被破坏了(因为它使用了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将一个对象放入缓冲区,然后调用"通知"。唯一等待的线程是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的缓冲区(使该示例易于理解)。以下步骤导致我们陷入僵局。请注意,无论何时使用notify唤醒线程,JVM都可以不确定地选择该线程,因为可以等待任何等待线程。还要注意,当多个线程在进入某个方法时正在阻塞(即尝试获取锁)时,获取的顺序可能是不确定的。还请记住,在任何时候,同步方法只允许一个线程在类中的任何(同步)方法中执行(即保持锁定),线程只能在其中一个方法中。如果发生以下事件序列,则会导致死锁:
步骤1:
P1将1个字符放入缓冲区
第2步:
P2尝试put
检查等待循环已经有一个char等待
步骤3:
P3尝试put
检查等待循环已经有一个char等待
第4步:
C1尝试获取1个字符
C2在进入get
方法时尝试获取1个char块
C3在进入get
方法时尝试获取1个char块
步骤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"。