Java:notify() 与 notifyAll() 重头再来

2024-02-17

如果一副护目镜“之间的差异notify() and notifyAll()”然后会弹出很多解释(抛开javadoc段落)。这一切都归结为被唤醒的等待线程的数量:一notify()和所有在notifyAll().

然而(如果我确实理解这些方法之间的区别的话),总是只选择一个线程来进一步获取监视器;在第一种情况下,由VM选择,在第二种情况下,由系统线程调度程序选择。程序员并不知道两者的确切选择过程(在一般情况下)。

什么是useful之间的区别notify() http://download.oracle.com/javase/6/docs/api/java/lang/Object.html#notify%28%29 and 通知全部() http://download.oracle.com/javase/6/docs/api/java/lang/Object.html#notifyAll%28%29然后?我错过了什么吗?


清楚地,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在生产者/消费者代码中(上面)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java:notify() 与 notifyAll() 重头再来 的相关文章

随机推荐