显然,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
,我们会得到 aIndexArrayOutOfBoundsException
因为 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获得锁,将一个char放入缓冲区,调用notify,退出方法
第 7 步:
- P2 的通知唤醒 P3(记住任何线程都可以被唤醒)
- P3 检查等待循环条件,缓冲区中已经有一个字符,所以等待。
- 没有更多的线程调用通知和三个线程永久暂停!
解决方案:在生产者/消费者代码中替换notify
为(上)。notifyAll