Java: notify() vs. notifyAll() all over again

2022-08-31 04:35:13

如果一个人在谷歌上搜索“和之间的差异”,那么会弹出很多解释(将javadoc段落分开)。这一切都归结为被唤醒的等待线程数:一个在 中,全部在 。notify()notifyAll()notify()notifyAll()

但是(如果我确实正确理解了这些方法之间的区别),则始终只选择一个线程进行进一步的监视器获取;在第一种情况下,VM 选择的是 VM 选择的那个,在第二种情况下,是系统线程计划程序选择的那个。程序员不知道它们的确切选择过程(在一般情况下)。

那么 notify()notifyAll() 之间的有用区别是什么呢?我错过了什么吗?


答案 1

显然,唤醒等待集中的(任意)一个线程,唤醒等待集中的所有线程。以下讨论应该可以消除任何疑问。 应该大部分时间使用。如果不确定要使用哪个,请使用 。请参阅以下说明。notifynotifyAllnotifyAllnotifyAll

仔细阅读并理解。如果您有任何疑问,请给我发电子邮件。

查看生产者/消费者(假设是具有两种方法的生产者消费者类)。它坏了(因为它使用) - 是的,它可能会工作 - 甚至大多数时候,但它也可能导致死锁 - 我们将看到为什么: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

使用者 1 (C1) 进入同步块,缓冲区为空,因此 C1 被放入等待集(通过调用)。使用者 2 (C2) 即将进入同步方法(在上面的 Y 点),但生产者 P1 将对象放入缓冲区中,随后调用 。唯一等待的线程是 C1,因此它被唤醒,现在尝试在点 X(上图)重新获取对象锁。waitnotify

现在,C1 和 C2 正在尝试获取同步锁。其中一个(非确定性地)被选中并进入方法,另一个被阻止(不是等待 - 而是被阻止,试图获取方法上的锁定)。假设 C2 首先获得锁定。C1 仍然阻塞(尝试在 X 处获取锁)。C2 完成该方法并释放锁。现在,C1 获取锁。猜猜看,幸运的是我们有一个循环,因为C1执行循环检查(guard),并被阻止从缓冲区中删除不存在的元素(C2已经得到了它!如果我们没有 ,我们将得到一个,因为 C1 试图从缓冲区中删除第一个元素!whilewhileIndexArrayOutOfBoundsException

现在

好了,现在我们为什么需要 notifyAll?

在上面的生产者/消费者示例中,看起来我们可以逃脱.看起来是这样,因为我们可以证明生产者和消费者的等待循环中的守卫是相互排斥的。也就是说,看起来我们不能在方法和方法中等待线程,因为要使该线程为真,则以下各项必须为真:notifyputget

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个字符 - 进入方法
时的块 - C3尝试获得1个字符 - 进入方法时的块getget

第5步:
- C1正在执行方法 - 获取char,调用,退出方法
- 唤醒P2
- 但是,C2在P2之前进入方法(P2必须重新获取锁),因此P2块在进入方法
时 - C2检查等待循环,缓冲区中没有更多的字符,所以等待
- C3在C2之后进入方法,但在P2之前, 检查等待循环,缓冲区中没有更多字符,因此等待getnotifynotifyput

第6步:
- 现在:有P3,C2和C3等待!
- 最后P2获取锁,在缓冲区中放置一个字符,调用通知,退出方法

第7步:
- P2的通知唤醒P3(记住任何线程都可以被唤醒)
- P3检查等待循环条件,缓冲区中已经有一个字符,所以等待。
- 没有更多的线程调用通知和三个线程永久挂起!

解决方案:替换为生产者/消费者代码(上文)。notifynotifyAll


答案 2

但是(如果我确实正确理解了这些方法之间的区别),则始终只选择一个线程进行进一步的监视器采集。

这是不正确的。 唤醒调用中被阻止的所有线程。线程只允许从一个接一个地返回,但它们每个线程都会轮到它们。o.notifyAll()o.wait()o.wait()


简而言之,这取决于您的线程等待通知的原因。您是想告诉其中一个等待线程发生了什么事情,还是想同时告诉所有线程?

在某些情况下,等待完成后,所有等待线程都可以采取有用的操作。一个例子是一组线程等待某个任务完成;任务完成后,所有等待的线程都可以继续其业务。在这种情况下,您可以使用 notifyAll() 同时唤醒所有等待的线程。

另一种情况,例如互斥锁定,只有一个等待线程在收到通知后可以执行有用的操作(在本例中为获取锁定)。在这种情况下,您更愿意使用 notify()。正确实现后,您也可以在这种情况下使用 notifyAll(),但会不必要地唤醒无论如何都无法执行任何操作的线程。


在许多情况下,等待条件的代码将编写为循环:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

这样,如果一个调用唤醒了多个等待线程,并且第一个从 make 返回的线程将条件保持在 false 状态,则被唤醒的其他线程将返回到等待状态。o.notifyAll()o.wait()