清楚地,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;
}
FIRSTLY,
为什么我们需要一个 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 试图从缓冲区中删除第一个元素!
NOW,
好吧,现在为什么我们需要notifyAll?
在上面的生产者/消费者示例中,看起来我们可以逃脱notify
。看起来是这样,因为我们可以证明,守卫在wait生产者和消费者的循环是互斥的。也就是说,看起来我们不能让线程在等待put
方法以及get
方法,因为要使这一点成立,则以下条件必须成立:
buf.size() == 0 AND buf.size() == MAX_SIZE
(假设MAX_SIZE不为0)
然而,这还不够好,我们需要使用notifyAll
。让我们看看为什么...
假设我们有一个大小为 1 的缓冲区(为了使示例易于理解)。以下步骤导致我们陷入僵局。请注意,任何时候线程被通知唤醒,它都可以由 JVM 不确定地选择 - 也就是说,任何等待的线程都可以被唤醒。另请注意,当多个线程在进入方法时阻塞(即尝试获取锁)时,获取的顺序可能是不确定的。还请记住,一个线程在任何时候只能位于其中一个方法中 - 同步方法只允许一个线程执行(即持有该类中的任何(同步)方法的锁)。如果发生以下事件序列 - 会导致死锁:
STEP 1:
- P1 将 1 个字符放入缓冲区
STEP 2:
- P2尝试put
- 检查等待循环 - 已经是一个字符 - 等待
STEP 3:
- P3尝试put
- 检查等待循环 - 已经是一个字符 - 等待
STEP 4:
- C1 尝试获取 1 个字符
- C2 尝试在进入时获取 1 个字符块get
method
- C3 尝试在进入时获取 1 个字符块get
method
STEP 5:
- C1 正在执行get
方法 - 获取字符,调用notify
, 退出方法
- The notify
唤醒P2
- 但是,C2 在 P2 之前进入方法(P2 必须重新获取锁),因此 P2 在进入方法时阻塞put
method
- C2 检查等待循环,缓冲区中没有更多字符,因此等待
- C3 在 C2 之后、P2 之前进入方法,检查等待循环,缓冲区中没有更多字符,因此等待
STEP 6:
- 现在:P3、C2 和 C3 正在等待!
- 最后P2获得锁,将一个字符放入缓冲区,调用notify,退出方法
STEP 7:
- P2的通知唤醒P3(记住任何线程都可以被唤醒)
- P3 检查等待循环条件,缓冲区中已经有一个字符,因此等待。
- 不再需要调用通知的线程,并且三个线程永久挂起!
解决方案:更换notify
with notifyAll
在生产者/消费者代码中(上面)。