Java:再次遍历notify()与notifyAll()


377

如果“之间的区别一个谷歌notify()notifyAll()”,那么很多的解释,会弹出(除了留下javadoc的段落)。这一切都归结于被唤醒等待的线程数:一个是notify()和所有notifyAll()

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

然后notify()notifyAll()有用区别是什么?我想念什么吗?


6
用于并发的有用库位于并发库中。我建议在几乎每种情况下,这些都是更好的选择。在并发性图书馆预日期的Java 5.0(其中5他们在2004年加入为标准)
彼得Lawrey

4
我不同意彼得。并发库是用Java实现的,每次调用lock(),unlock()等都会执行很多Java代码。您可以通过使用并发库来代替自己,而不是使用old old synchronized,但某些情况除外,相当罕见的用例。
亚历山大·里佐夫

2
关键的误解似乎是这样的:...总是只选择一个线程来进一步获取监视器;在第一种情况下,由VM选择,在第二种情况下,由系统线程调度程序选择。含义是基本上相同。尽管所描述的行为是正确的,但缺少的是,在这种notifyAll()情况下,_第一个线程之后的其他线程保持唤醒状态,并且将一个接一个地获取监视器。在这种notify情况下,其他任何线程都不会被唤醒。所以在功能上它们是完全不同的!
BeeOnRope

1)如果一个对象上有许多线程在等待,并且notify()在该对象上仅被调用一次。除了其中一个等待线程之外,其余线程是否永远等待?2)如果使用notify(),则只有许多等待线程中的一个开始执行。如果使用notifyall(),则所有等待线程都被通知,但是只有其中一个开始执行,那么notifyall()的用途是什么?
Chetan Gowda

@ChetanGowda通知所有线程与仅通知一个任意线程实际上实际上有显着差异,直到看似微妙但重要的差异出现为止。当notify()仅一个线程时,所有其他线程将处于等待状态,直到收到明确的通知/信号。通知所有线程后,所有线程将按一个顺序一个接一个地执行和完成,而无需任何进一步的通知-在这里我们应该说线程blocked不是,waitingblocked它的exec临时挂起直到另一个线程进入该sync块时。
user104309

Answers:


248

但是(如果我确实正确理解了这两种方法之间的区别),则始终仅选择一个线程来进行进一步的监视器获取。

那是不对的。 o.notifyAll()唤醒所有o.wait()调用中阻塞的线程。线程只能o.wait()一一返回,但每个线程都会轮到自己。


简而言之,这取决于您的线程为何等待被通知。您是否要告诉一个正在等待的线程发生了什么,还是要同时告诉所有这些线程?

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

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


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

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

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


29
如果仅通知一个线程但有多个线程在等待一个对象,则VM如何确定要通知哪个线程?
两栖游戏,

6
我不能肯定地说Java规范,但是通常应该避免对这些细节进行假设。我认为您可以假设VM将以理智且主要是公平的方式来执行此操作。
利德曼

15
Liedman是严重错误的,Java规范明确声明了notify()不能保证是公平的。即,每次要通知的调用都可能再次唤醒同一线程(监视器中的线程队列为NOT FAIR或FIFO)。但是,调度程序保证是公平的。这就是为什么在大多数情况下您拥有两个以上线程的原因,您应该首选notifyAll。
Yann TM

45
@YannTM我全力支持建设性的批评,但是我认为您的语气有点不公平。我明确地说过“不能确定”和“我认为”。放松一下,您是否曾经写过七年前不是100%正确的东西?
利德曼2015年

10
问题在于这是公认的答案,而不是个人自豪感的问题。如果您知道自己现在错了,请编辑您的答案说出来,并在下面指向例如xagyg教学法和正确答案。
Yann TM

330

显然,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,则IndexArrayOutOfBoundsExceptionC1尝试从缓冲区中删除第一个元素时将得到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检查等待循环条件,缓冲区中已经有一个字符,因此等待。
-没有更多的消息可以通知,并且三个线程将永久暂停!

解决办法:更换notifynotifyAll在生产者/消费者代码(上文)。


1
finnw-P3必须重新检查条件,因为notify导致P3(在此示例中为所选线程)从其等待的点(即while循环内)继续进行。还有其他一些示例不会导致死锁,但是,在这种情况下,使用notify不能保证无死锁的代码。使用的notifyAll
xagyg's

4
@marcus非常接近。使用notifyAll,每个线程将重新获取锁(一次获取一个),但是请注意,在一个线程重新获取锁并执行该方法(然后退出)之后,…下一个线程重新获取锁,检查“ while”并返回“等待”状态(当然取决于情况)。因此,notify唤醒一个线程-正确声明状态。notifyAll唤醒所有线程,并且每个线程一次重新获取一个锁-检查“ while”的条件,然后再次执行该方法或“ waits”。
xagyg 2011年

1
@xagyg,您是在谈论每个生产者只能存储一个字符的情况吗?如果是这样,您的示例是正确的,但不是非常有趣的IMO。通过建议的其他步骤,您可以使同一系统死锁,但输入量却是无限的,这实际上就是通常使用的这种模式。
eran 2011年

3
@codeObserver您询问:“将调用notifyAll()导致多个等待线程同时检查while()条件..因此,有可能在对while进行满足之前,有2个线程已经退出,导致outOfBound例外?” 不可以,这是不可能的,因为尽管会唤醒多个线程,但它们无法同时检查while条件。他们每个人都必须先重新获得锁定(等待之后立即),然后才能重新进入代码部分并重新检查时间。因此,一次一次。
xagyg

4
@xagyg很好的例子。这与原始问题无关。只是为了讨论。僵局是一个设计问题imo(如果我错了,请纠正我)。因为您的put和get共享一个锁。而且,JVM不够聪明,无法在获取释放后调用put来进行锁定,反之亦然。之所以发生死锁,是因为put唤醒了另一个put,由于while()而使自身返回到wait()。使两个类(和两个锁)起作用吗?所以put {synchonized(get)},get {(synchonized(put)}。换句话说,get将仅唤醒put,而put将仅唤醒get
Jay

43

有用的区别:

  • 如果所有等待线程都可以互换(唤醒顺序无关紧要),或者只有一个等待线程,请使用notify()。一个常见的示例是用于执行队列中作业的线程池-添加作业后,通知其中一个线程唤醒,执行下一个作业并返回睡眠状态。

  • 在其他情况下,等待线程可能具有不同的用途并且应能够并行运行,请使用notifyAll()。一个示例是对共享资源的维护操作,其中多个线程在访问该资源之前正在等待操作完成。


19

我认为这取决于资源的生产和消费方式。如果一次有5个工作对象可用,而您有5个使用者对象,则可以使用notifyAll()唤醒所有线程,以便每个线程可以处理1个工作对象。

如果只有一个工作对象可用,唤醒所有消费者对象以争用该对象有什么意义?第一个检查可用工作的程序将获得此权限,所有其他线程将检查并发现它们无关。

我在这里找到了很好的解释。简而言之:

notify()方法通常用于资源池资源池中有任意数量的“消费者”或“工人”来获取资源,但是将资源添加到池中时,只有等待中的消费者或工人可以处理用它。实际上,其他大多数情况下都使用notifyAll()方法。严格地,需要将可能允许多个侍者继续前进的情况通知侍者。但这通常很难知道。因此,通常来说,如果您没有使用notify()的特定逻辑,那么您可能应该使用notifyAll(),因为通常很难确切地知道哪些线程将在特定对象上等待,为什么。


11

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

Doug Lea的带来了他的一个有趣的名著:如果notify()Thread.interrupt()发生在同一时间,该通知实际上可能迷路。notifyAll()即使您付出了开销的代价(大多数情况下会消耗太多线程),但如果发生这种情况并产生重大影响则是一个更安全的选择。


10

简短的摘要:

除非您有一个大型并行应用程序,其中大量线程都在执行相同的操作,否则始终始终使用notifyAll()而不是notify()

说明:

notify() [...]唤醒单个线程。因为notify()不允许您指定被唤醒的线程,所以它仅在大规模并行应用程序中有用,也就是说,具有大量线程的程序都执行相似的杂务。在这样的应用程序中,您不必担心哪个线程被唤醒。

来源:https : //docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

在上述情况下,将notify()notifyAll()进行比较:一个大型并行应用程序,其中线程在做相同的事情。如果在这种情况下调用notifyAll(),则notifyAll()会导致大量线程的唤醒(即调度),其中许多线程是不必要的(因为实际上只有一个线程可以继续进行,即将被授予运行权的线程)。监视对象wait()notify()notifyAll()被调用),因此浪费了计算资源。

因此,如果您没有大量线程同时执行相同操作的应用程序,则优先使用notifyAll()而不是notify()。为什么?因为,正如其他用户在此论坛中已经回答的那样,notify()

唤醒正在此对象的监视器上等待的单个线程。[...]选择是任意的,并且由实现自行决定。

来源:Java SE8 API(https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify--

假设您有一个生产者消费者应用程序,其中消费者准备就绪(例如,wait() ing)可以消费,生产者准备就绪(即,wait() ing)可以生产,并且待生产(消费/生产)的物品队列为空。在那种情况下,notify()可能只会唤醒使用者,而不会唤醒生产者,因为唤醒者的选择是任意的。尽管生产者和消费者分别准备生产和消费,但生产者消费者周期不会取得任何进展。相反,消费者被唤醒(即保持wait()状态),因为物品为空而不会将其从队列中取出,并通知另一个消费者继续进行。

相反,notifyAll()会同时唤醒生产者和消费者。计划谁的选择取决于计划程序。当然,根据调度程序的实现,调度程序也可能仅调度使用者(例如,如果为使用者线程分配非常高的优先级)。但是,这里的假设是,调度程序仅调度使用者的危险性低于JVM仅唤醒使用者的危险性,因为任何合理实现的调度器都不会做出任意决定。而是,大多数调度程序实现都至少做了一些努力来防止饥饿。


9

这是一个例子。运行。然后将notifyAll()之一更改为notify(),看看会发生什么。

ProducerConsumerExample类

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Dropbox类

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

消费阶层

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

生产者阶层

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}

8

来自Java专家Joshua Bloch的《有效Java第二版》:

“项目69:建议并发实用程序等待并通知”。


16
为什么比源更重要。
Pacerier 2014年

2
@Pacerier好说。我也会更想找出原因。一个可能的原因可能是对象类中的等待和通知基于隐式条件变量。因此,在标准的生产者和消费者示例中……生产者和消费者都将在相同的条件下等待,这可能导致僵局,正如xagyg在其回答中所解释的那样。因此,更好的方法是使用2个条件变量,如docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…所述
rahul

6

我希望这将消除一些疑问。

notify():notify()方法唤醒一个等待锁定的线程(第一个在该锁定上调用wait()的线程)。

notifyAll():notifyAll()方法唤醒所有等待锁定的线程。JVM从等待锁定的线程列表中选择一个线程,然后唤醒该线程。

在单个线程等待锁定的情况下, notify()和notifyAll()之间没有显着差异。但是,当有多个线程在等待锁时,在notify()和notifyAll()中,唤醒的确切线程都在JVM的控制之下,并且您无法以编程方式控制唤醒特定线程。

乍一看,似乎最好只调用notify()来唤醒一个线程。似乎没有必要唤醒所有线程。但是,notify()的问题在于,唤醒的线程可能不适合被唤醒(线程可能正在等待其他条件,或者该线程仍然不满足该条件,等等)。在那种情况下,notify()可能会丢失,并且没有其他线程会唤醒,从而可能导致某种类型的死锁(通知丢失,并且所有其他线程一直在等待通知)。

为避免此问题,当有多个线程在等待锁(或有多个等待条件)时,最好调用notifyAll()。notifyAll()方法唤醒所有线程,因此效率不高。但是,在实际应用中,这种性能损失可以忽略不计。


6

线程有三种状态。

  1. 等待-线程未使用任何CPU周期
  2. BLOCKED-试图获取监视器的线程被阻塞。它可能仍在使用CPU周期
  3. RUNNING-线程正在运行。

现在,当调用notify()时,JVM将选择一个线程并将其移至BLOCKED状态,从而移至RUNNING状态,因为没有与监视对象的竞争。

调用notifyAll()时,JVM会选择所有线程并将它们全部移到BLOCKED状态。所有这些线程将优先获得对象的锁定。首先能够获取监视器的线程将能够首先进入RUNNING状态,依此类推。


很棒的解释。
royatirek

5

令我惊讶的是,没有人提到臭名昭著的“丢失的唤醒”问题(用Google搜索)。

基本上:

  1. 如果您有多个线程在相同条件下等待,并且,
  2. 多个线程可以使您从状态A转换为状态B,并且
  3. 多个线程可以使您从状态B转换到状态A(通常与1中的线程相同),并且
  4. 从状态A转换为B时应通知1中的线程。

然后,应该使用notifyAll,除非您有可保证的保证,不可能丢失唤醒。

一个常见的示例是并发FIFO队列,其中:多个排队器(上面的1.和3.)可以将您的队列从空过渡到非空多个出队列器(上面的2.)可以等待条件“队列不为空”为空->非空应通知出队

您可以轻松地编写一个操作交错,其中从一个空队列开始,有2个入队者和2个出队者进行交互,并且1个入队者将保持睡眠状态。

这个问题可以说与死锁问题相当。


抱歉,xagyg对其进行了详细说明。问题的名称是“丢失的唤醒”
NickV 2012年

@Abhay Bansal:我认为您错过了condition.wait()释放锁并被唤醒的线程重新获取的事实。
NickV 2012年

4

notify()将唤醒一个线程,同时notifyAll()将唤醒所有线程。据我所知,没有中间立场。但是,如果不确定如何处理notify()线程,请使用notifyAll()。每次都像魅力一样。


4

就我所知,以上所有答案都是正确的,因此我将告诉您其他内容。对于生产代码,您确实应该使用java.util.concurrent中的类。在Java的并发方面,它们对您无能为力。


4

notify()使您可以编写比效率更高的代码notifyAll()

请考虑以下从多个并行线程执行的代码:

synchronized(this) {
    while(busy) // a loop is necessary here
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notifyAll();
}

使用notify()以下命令可以使它更有效:

synchronized(this) {
    if(busy)   // replaced the loop with a condition which is evaluated only once
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notify();
}

如果您有大量线程,或者等待循环条件的评估成本很高,notify()则速度将明显快于notifyAll()。例如,如果您有1000个线程,则在第一个线程之后将唤醒并评估999个线程notifyAll(),然后998,然后997等。相反,通过该notify()解决方案,将仅唤醒一个线程。

notifyAll()在需要选择下一步执行工作的线程时使用:

synchronized(this) {
    while(idx != last+1)  // wait until it's my turn
        wait();
}
...
synchronized(this) {
    last = idx;
    notifyAll();
}

最后,重要的是要了解,在的情况下notifyAll()synchronized已唤醒的块内的代码将按顺序执行,而不是一次执行。假设在上面的示例中有三个线程在等待,第四个线程在调用notifyAll()。所有三个线程将被唤醒,但是只有一个线程将开始执行并检查while循环条件。如果条件为true,它将wait()再次调用,然后第二个线程将开始执行并检查其while循环条件,依此类推。


4

这是一个更简单的解释:

您是正确的,无论您使用notify()还是notifyAll(),直接的结果就是恰好另一个线程将获取该监视器并开始执行。(假设实际上有一些线程在此对象的wait()上被阻塞,其他不相关的线程没有吸收所有可用的内核,等等。)影响是稍后出现的。

假设线程A,B和C正在等待该对象,并且线程A获取了监视器。区别在于,一旦A释放监视器,将会发生什么。如果您使用notify(),那么B和C仍然在wait()中被阻塞:它们没有在监视器上等待,而是在等待被通知。当A释放监视器时,B和C仍然坐在那里,等待notify()。

如果您使用notifyAll(),则B和C都已超过“等待通知”状态,并且都在等待获取监视器。当A释放监视器时,B或C都将获取它(假定没有其他线程在争夺该监视器)并开始执行。


非常清楚的解释。notify()的这种行为的结果可能导致“缺少信号” /“缺少通知”,从而导致应用程序状态死锁/无进展情况。P-生产者,C-消费者P1,P2和C2正在等待C1。C1调用了notify()并用于生产者,但是C2可以被唤醒,因此P1和P2都错过了该通知,并且将等待进一步的显式“通知”(notify()调用)。
user104309

4

此答案是xagyg的出色答案的图形化重写和简化,包括eran的评论

即使每种产品都面向单个消费者,为什么还要使用notifyAll?

考虑生产者和消费者,简化如下。

制片人:

while (!empty) {
   wait() // on full
}
put()
notify()

消费者:

while (empty) {
   wait() // on empty
}
take()
notify()

假设2个生产者和2个消费者共享一个大小为1的缓冲区。下图描述了导致死锁的情况,如果所有线程都使用notifyAll,则可以避免死锁

每个通知都标有正在唤醒的线程。

通知死锁


3

我想提一下Java Concurrency in Practice中的解释:

第一点,是Notify还是NotifyAll?

It will be NotifyAll, and reason is that it will save from signall hijacking.

如果两个线程A和B在同一条件队列的不同条件谓词上等待并调用notify,那么线程JVM将向其通知的是JVM。

现在,如果notify是针对线程A的,而JVM是通知线程B的,则线程B将唤醒,并看到此通知没有用,因此它将再次等待。而且线程A永远不会知道这个错过的信号,有人劫持了它的通知。

因此,调用notifyAll将解决此问题,但再次会影响性能,因为它将通知所有线程,并且所有线程将争夺同一锁,并且将涉及上下文切换并因此导致CPU负载。但是,我们仅应在性能正常的情况下关心性能,如果其行为本身不正确,则性能没有用。

可以使用jdk 5中提供的显式锁定Lock的Condition对象解决此问题,因为它为每个条件谓词提供了不同的等待时间。在这里它将正常运行,并且不会出现性能问题,因为它将调用signal并确保只有一个线程在等待该条件


3

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

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

BLOCKED-阻止获取锁。 WAITING-等待通知(或阻止加入完成)。


3

取自有效Java 博客

The notifyAll method should generally be used in preference to notify. 

If notify is used, great care must be taken to ensure liveness.

因此,我了解的是(从上述博客中,“ Yann TM”对已接受的答案和Java 文档发表评论):

  • notify():JVM唤醒此对象上的一个等待线程。线程选择是任意选择的,没有公平性。因此,可以一次又一次唤醒同一线程。因此,系统的状态发生了变化,但是没有取得真正的进展。从而创建一个活锁
  • notifyAll():JVM唤醒所有线程,然后所有线程争夺该对象的锁定。现在,CPU调度程序选择一个线程来获取对该对象的锁定。该选择过程将比通过JVM进行选择要好得多。因此,确保了生活。

2

看一下@xagyg发布的代码。

假设两个不同的线程正在等待两个不同的条件:
一个线程正在等待buf.size() != MAX_SIZE第二个线程正在等待buf.size() != 0

假设在某个时候buf.size() 不等于0。JVM调用notify()而不是notifyAll(),并且第一个线程被通知(而不是第二个)。

第一个线程被唤醒,检查buf.size()哪个线程可能返回MAX_SIZE,然后返回等待状态。第二个线程未唤醒,继续等待并且未调用get()


1

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

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

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


13
如果notify()不是完全是“ 第一个线程 ”。
Bhesh Gurung 2011年

6
您无法预测VM将选择哪个。只有天知道。
Sergii Shevchyk 2012年

无法保证谁会成为第一个(不公平)
Ivan Voroshilin

只有在操作系统保证的情况下,它才会唤醒第一个线程,并且可能不会。它实际上是根据OS(及其调度程序)来确定唤醒哪个线程的。
Paul Stelian

1

notify将仅通知一个处于等待状态的线程,而notify all将通知所有处于等待状态的线程,现在所有已通知的线程和所有被阻止的线程都可以使用该锁,只有其中一个将获得该锁,并且所有其他人(包括较早处于等待状态的人)将处于阻止状态。


1

总结上述出色的详细解释,并且以我能想到的最简单的方式,这是由于JVM内置监视器的局限性,其中1)是在整个同步单元(块或对象)上获取的,而2)不区分正在等待/通知的特定条件。

这意味着,如果多个线程正在等待不同的条件并使用notify(),则所选线程可能不是在新满足的条件下会取得进展的线程-导致该线程(以及其他可能仍在等待的线程满足条件等。)无法取得进展,最终导致饥饿或程序中断。

相反,notifyAll()允许所有等待线程最终重新获取该锁并检查它们各自的条件,从而最终允许取得进展。

因此,仅在保证有任何等待线程允许进行选择的情况下才可以安全地使用notify(),通常,当同一监视器内的所有线程仅检查一个相同条件时,通常会满足该条件(相当少见)在现实世界中的应用案例。


0

当您调用“对象”的wait()(希望已获得对象锁)时,实习生将释放该对象上的锁,并帮助其他线程对该对象进行锁,在这种情况下,超过1个线程在等待“资源/对象”(考虑其他线程也在上面的同一对象上发出了等待,然后将有一个线程填充资源/对象并调用notify / notifyAll)。

在这里,当您发出相同对象的通知时(从进程/代码的同一/另一端),这将释放一个阻塞的并等待的单线程(不是所有等待的线程-JVM线程将选择此释放的线程)调度程序和对象上的所有锁获取过程与常规相同)。

如果只有一个线程将在此对象上共享/工作,则可以在wait-notify实现中单独使用notify()方法。

如果您处于根据业务逻辑在资源/对象上读写多个线程的情况下,则应使用notifyAll()

现在我正在寻找当我们在对象上发出notify()时,jvm到底是如何识别并打破等待线程的。


0

尽管上面有一些可靠的答案,但我对我所读到的许多困惑和误解感到惊讶。这可能证明了这样一种想法,即应该尽可能多地使用java.util.concurrent,而不是尝试编写自己的损坏的并发代码。回到问题:总而言之,当今的最佳实践是由于丢失的唤醒问题,在所有情况下都要避免notify()。不了解这一点的任何人都不应编写关键任务并发代码。如果您担心放牧问题,一次唤醒一个线程的一种安全方法是:1.为等待的线程建立一个明确的等待队列;2.让队列中的每个线程等待它的前任;3.完成后,让每个线程调用notifyAll()。或者,您可以使用Java.util.concurrent。*,


以我的经验,等待/通知常用于队列机制,其中线程(Runnable实现)处理队列的内容。的wait(),则使用每当队列是空的。并且在notify()添加信息时称为。->在这种情况下,只有1个线程曾经调用过wait(),那么notifyAll()如果您知道只有1个等待线程,那么使用a看起来就有点愚蠢了。
bvdb

-2

唤醒所有人在这里没有多大意义。等待notify和notifyall,所有这些都放在拥有对象的监视器之后。如果某个线程处于等待阶段并调用notify,则该线程将占用该锁,并且此时其他任何线程都无法占用该锁。因此并发访问根本不会发生。据我所知,任何等待通知和notifyall的呼叫只有在锁定对象后才能进行。如果我错了请纠正我。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.