显然,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尝试从缓冲区中删除第一个元素时将得到a !
现在,
好的,现在为什么我们需要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尝试获取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
在生产者/消费者代码(上文)。