如果一个人在谷歌上搜索“notify()和notifyAll()之间的区别”,那么会跳出很多解释(撇开javadoc段落)。这都归结于被唤醒的等待线程的数量:notify()中有一个,notifyAll()中有所有线程。

然而(如果我确实理解了这些方法之间的区别),只有一个线程总是被选择用于进一步的监视采集;第一种情况是VM选择的线程,第二种情况是系统线程调度程序选择的线程。程序员不知道它们的确切选择过程(在一般情况下)。

那么notify()和notifyAll()之间有什么有用的区别呢?我遗漏了什么吗?


当前回答

等待队列和阻塞队列

您可以假设与每个锁对象关联的队列有两种类型。一个是阻塞队列,包含等待监控器锁的线程,另一个是等待队列,包含等待通知的线程。(线程调用Object.wait时将被放入等待队列)。

每次锁可用时,调度器从阻塞队列中选择一个线程执行。

当调用notify时,等待队列中只有一个线程被放入阻塞队列中争夺锁,而notifyAll将等待队列中的所有线程放入阻塞队列中。

现在你能看出区别了吗? 尽管在这两种情况下,只有一个线程被执行,但使用notifyAll,其他线程仍然得到一个要执行的更改(因为它们在阻塞队列中),即使它们未能争用锁。

一些指导原则

我基本上建议一直使用notifyAll,尽管可能会有一点性能损失。 仅在以下情况下使用notify:

任何被唤醒的线程都可以使程序继续运行。 性能很重要。

例如: @xagyg的回答给出了一个通知会导致死锁的例子。在他的例子中,生产者和消费者都与同一个锁对象相关。因此,当生产者调用notify时,可以通知生产者或消费者。但是,如果一个生产者被唤醒,它就不能使程序继续进行,因为缓冲区已经满了。因此发生了死锁。 有两种解决方法:

使用@xagyg建议的notifyALl。 使生产者和消费者关联不同的锁对象,并且生产者只能唤醒消费者,消费者只能唤醒生产者。在这种情况下,无论唤醒哪个消费者,它都可以消费缓冲区并使程序继续进行。

其他回答

显然,notify唤醒等待集中的一个线程(any), notifyAll唤醒等待集中的所有线程。下面的讨论应能消除任何疑问。大多数时候应该使用notifyAll。如果您不确定使用哪个,那么使用notifyAll。请看下面的解释。

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

查看生产者/消费者(假设是一个具有两个方法的ProducerConsumer类)。IT IS BROKEN(因为它使用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被放入等待集(通过等待调用)。消费者2 (C2)即将进入同步方法(在上面的Y点),但生产者P1在缓冲区中放入一个对象,然后调用notify。唯一等待的线程是C1,因此它被唤醒,现在试图重新获得点X(上面)的对象锁。

Now C1 and C2 are attempting to acquire the synchronization lock. One of them (nondeterministically) is chosen and enters the method, the other is blocked (not waiting - but blocked, trying to acquire the lock on the method). Let's say C2 gets the lock first. C1 is still blocking (trying to acquire the lock at X). C2 completes the method and releases the lock. Now, C1 acquires the lock. Guess what, lucky we have a while loop, because, C1 performs the loop check (guard) and is prevented from removing a non-existent element from the buffer (C2 already got it!). If we didn't have a while, we would get an IndexArrayOutOfBoundsException as C1 tries to remove the first element from the buffer!

NOW,

为什么我们需要notifyAll?

在上面的生产者/消费者示例中,我们似乎可以使用notify。看起来是这样的,因为我们可以证明生产者和消费者的等待循环上的守卫是互斥的。也就是说,看起来我们不能让一个线程同时在put方法和get方法中等待,因为,为了使它为真,那么下面的条件必须为真:

buf.size() == 0 AND buf.size() == MAX_SIZE(假设MAX_SIZE不为0)

然而,这还不够好,我们需要使用notifyAll。让我们看看为什么……

Assume we have a buffer of size 1 (to make the example easy to follow). The following steps lead us to deadlock. Note that ANYTIME a thread is woken with notify, it can be non-deterministically selected by the JVM - that is any waiting thread can be woken. Also note that when multiple threads are blocking on entry to a method (i.e. trying to acquire a lock), the order of acquisition can be non-deterministic. Remember also that a thread can only be in one of the methods at any one time - the synchronized methods allow only one thread to be executing (i.e. holding the lock of) any (synchronized) methods in the class. If the following sequence of events occurs - deadlock results:

步骤1: - P1将1个字符放入缓冲区

步骤2: P2尝试put -检查等待循环-已经是一个字符-等待

步骤3: P3尝试put -检查等待循环-已经是一个字符-等待

步骤4: - C1尝试获取1个char C2尝试在进入get方法时获取1个字符块 C3尝试在进入get方法时获取1个字符块

步骤5: C1正在执行get方法——获取char,调用notify,退出方法 —notify唤醒P2 但是,C2在P2之前进入方法(P2必须重新获得锁),所以P2在进入put方法时阻塞 C2检查等待循环,缓冲区中没有更多字符,所以等待 C3在C2之后进入方法,但在P2之前,检查等待循环,缓冲区中没有更多字符,所以等待

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

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

解决方案:在生产者/消费者代码(上面)中用notifyAll替换notify。

notify() -从对象的等待集中随机选择一个线程,并将其置于BLOCKED状态。对象的等待集中的其余线程仍然处于WAITING状态。

notifyAll() -将所有线程从对象的等待集移动到BLOCKED状态。使用notifyAll()后,共享对象的等待集中没有剩余线程,因为所有线程现在都处于BLOCKED状态,而不是WAITING状态。

BLOCKED—锁定获取阻塞。 WAITING -等待通知(或阻塞连接完成)。

notify()将唤醒一个线程,而notifyAll()将唤醒所有线程。据我所知,没有中间立场。但是如果你不确定notify()会对你的线程做什么,使用notifyAll()。每次都很灵验。

Notify()唤醒在同一对象上调用wait()的第一个线程。

notifyAll()唤醒在同一对象上调用wait()的所有线程。

优先级最高的线程将首先运行。

注意,对于并发实用程序,您还可以在signal()和signalAll()之间进行选择,因为在那里调用了这些方法。因此,即使使用java.util.concurrent,这个问题仍然有效。

Doug Lea在他著名的书中提出了一个有趣的观点:如果notify()和Thread.interrupt()同时发生,通知实际上可能会丢失。如果可能发生这种情况,并且有显著的影响,notifyAll()是一个更安全的选择,即使您付出了开销的代价(大多数时间唤醒太多线程)。