Java: notify () против notifyAll () снова и снова - PullRequest
347 голосов
/ 31 августа 2008

Если один из Googles для «разницы между notify() и notifyAll()», то появится много объяснений (оставляя в стороне абзацы javadoc). Все сводится к числу ожидающих потоков: один в notify() и все в notifyAll().

Однако (если я правильно понимаю разницу между этими методами), всегда выбирается только один поток для дальнейшего получения монитора; в первом случае тот, который выбран виртуальной машиной, во втором случае тот, который выбран планировщиком системного потока. Точные процедуры выбора для них обоих (в общем случае) не известны программисту.

Что такое полезная разница между notify () и notifyAll () тогда? Я что-то упустил?

Ответы [ 26 ]

314 голосов
/ 06 июля 2010

Очевидно, 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 выше), но Producer P1 помещает объект в буфер и впоследствии вызывает notify. Единственный ожидающий поток - это C1, поэтому он проснулся и теперь пытается повторно получить блокировку объекта в точке X (выше).

Теперь C1 и C2 пытаются получить блокировку синхронизации. Один из них (недетерминированный) выбирается и входит в метод, другой блокируется (не ожидает - но блокируется, пытаясь получить блокировку для метода). Допустим, C2 сначала получает блокировку. C1 все еще блокирует (пытается захватить блокировку в X). C2 завершает метод и снимает блокировку. Теперь С1 получает блокировку. Угадайте, что, к счастью, у нас есть цикл while, потому что C1 выполняет проверку цикла (защита) и не может удалить несуществующий элемент из буфера (C2 уже получил его!). Если бы у нас не было while, мы получили бы IndexArrayOutOfBoundsException, поскольку C1 пытается удалить первый элемент из буфера!

сейчас

Хорошо, теперь зачем нам уведомлять все?

В приведенном выше примере производителя / потребителя это выглядит так, как будто мы можем сойти с notify Кажется, это так, потому что мы можем доказать, что охранники в циклах wait для производителя и потребителя являются взаимоисключающими. То есть, похоже, что у нас не может быть потока, ожидающего в методе put, а также в методе get, потому что для того, чтобы это было верно, тогда должно быть верно следующее:

buf.size() == 0 AND buf.size() == MAX_SIZE (предположим, MAX_SIZE не равен 0)

ОДНАКО, этого недостаточно, нам НУЖНО использовать notifyAll. Посмотрим почему ...

Предположим, у нас есть буфер размером 1 (чтобы сделать пример простым для подражания). Следующие шаги ведут нас в тупик. Обратите внимание, что ЛЮБОЙ поток просыпается с уведомлением, он может быть недетерминированно выбран JVM - то есть любой ожидающий поток может быть разбужен. Также обратите внимание, что, когда несколько потоков блокируют вход в метод (то есть пытается получить блокировку), порядок получения может быть недетерминированным. Помните также, что поток может быть только в одном из методов в любой момент времени - синхронизированные методы позволяют только одному потоку выполнять (т.е. удерживать блокировку) любые (синхронизированные) методы в классе. Если происходит следующая последовательность событий - получается тупик:

ШАГ 1:
- P1 помещает 1 символ в буфер

ШАГ 2:
- P2 пытается put - проверяет цикл ожидания - уже символ - ждет

ШАГ 3:
- P3 пытается put - проверяет цикл ожидания - уже символ - ждет

ШАГ 4:
- С1 пытается получить 1 символ
- C2 пытается получить 1 char - блоки при входе в метод get
- C3 пытается получить 1 char - блоки при входе в метод get

ШАГ 5:
- C1 выполняет метод get - получает символ, вызывает notify, выходит из метода
- notify просыпается P2
- НО, C2 вводит метод до того, как P2 сможет (P2 должен повторно захватить блокировку), поэтому P2 блокируется при входе в метод put
- C2 проверяет цикл ожидания, в буфере больше нет символов, поэтому ждет
- C3 входит в метод после C2, но перед P2 проверяет цикл ожидания, в буфере больше нет символов, поэтому ждет

ШАГ 6:
- СЕЙЧАС: P3, C2 и C3 ждут!
- Наконец P2 получает блокировку, помещает символ в буфер, вызывает notify, выходит из метода

ШАГ 7:
- Уведомление P2 пробуждает P3 (помните, что любой поток может быть разбужен)
- P3 проверяет состояние цикла ожидания, в буфере уже есть символ, поэтому ждет.
- НЕТ БОЛЬШЕ НИТЕЙ, КОТОРЫЕ ВЫЗВАТЬ, УВЕДОМЛЯЮТ, ТРИ НИТИ ПОСТОЯННО ПРИВЕДЕНЫ!

РЕШЕНИЕ: замените notify на notifyAll в коде производителя / потребителя (выше).

241 голосов
/ 31 августа 2008

Тем не менее (если я правильно понимаю разницу между этими методами), только один поток всегда выбирается для дальнейшего получения монитора.

Это не правильно. o.notifyAll() пробуждает все потоков, заблокированных в o.wait() вызовах. Потокам разрешено возвращаться только из o.wait() один за другим, но каждый из них получит свой ход.


Проще говоря, это зависит от того, почему ваши темы ожидают уведомления. Вы хотите сообщить одному из ожидающих потоков, что что-то произошло, или вы хотите рассказать обо всех одновременно?

В некоторых случаях все ожидающие потоки могут предпринять полезные действия после завершения ожидания. Примером может служить набор потоков, ожидающих завершения определенной задачи; По завершении задачи все ожидающие потоки могут продолжить свою работу. В таком случае вы будете использовать notifyAll () , чтобы разбудить все ожидающие потоки одновременно.

В другом случае, например, взаимоисключающей блокировке, только один из ожидающих потоков может сделать что-то полезное после получения уведомления (в этом случае получить блокировку). В таком случае вам лучше использовать notify () . При правильной реализации вы могли бы использовать notifyAll () и в этой ситуации, но вы без необходимости будите потоки, которые все равно ничего не могут сделать.


Во многих случаях код для ожидания условия будет записан в виде цикла:

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

Таким образом, если вызов o.notifyAll() пробуждает более одного ожидающего потока, а первый, который возвращается из o.wait(), оставляет условие в ложном состоянии, то остальные пробужденные потоки возвращаются к ожидание.

40 голосов
/ 31 августа 2008

Полезные отличия:

  • Используйте notify () , если все ваши ожидающие потоки являются взаимозаменяемыми (порядок их пробуждения не имеет значения), или если у вас есть только один ожидающий поток. Типичным примером является пул потоков, используемый для выполнения заданий из очереди - при добавлении задания один из потоков получает уведомление о пробуждении, выполнении следующего задания и возвращении в спящий режим.

  • Используйте notifyAll () для других случаев, когда ожидающие потоки могут иметь разные цели и должны иметь возможность работать одновременно. Примером является операция обслуживания общего ресурса, когда несколько потоков ожидают завершения операции, прежде чем получить доступ к ресурсу.

18 голосов
/ 31 августа 2008

Я думаю, это зависит от того, как производятся и потребляются ресурсы. Если одновременно доступны 5 рабочих объектов и у вас есть 5 потребительских объектов, имеет смысл разбудить все потоки с помощью notifyAll (), чтобы каждый мог обработать 1 рабочий объект.

Если у вас есть только один доступный рабочий объект, какой смысл в том, чтобы разбудить все потребительские объекты в гонке за этот один объект? Первая, проверяющая доступную работу, получит ее, а все остальные потоки проверит и обнаружат, что им нечего делать.

Я нашел отличное объяснение здесь . Короче говоря:

Обычно используется метод notify () для пулов ресурсов , где есть произвольное количество "потребителей" или «рабочие», которые берут ресурсы, но когда ресурс добавлен в пул, только один из ожидающих потребителей или рабочие могут иметь дело с этим. Метод notifyAll () фактически используется в большинство других случаев. Строго говоря, это необходимо уведомить официантов о состояние, которое может позволить несколько официанты, чтобы продолжить. Но это часто трудно узнать. Так что в целом правило, , если у вас нет конкретного логика для использования notify (), то вы вероятно, следует использовать notifyAll () , потому что это часто трудно узнать какие именно темы будут ждать на конкретный объект и почему.

11 голосов
/ 17 июня 2009

Обратите внимание, что с утилитами параллелизма у вас также есть выбор между signal() и signalAll(), поскольку эти методы вызываются там. Таким образом, вопрос остается в силе даже при java.util.concurrent.

Даг Ли поднимает интересный момент в своей знаменитой книге : если notify() и Thread.interrupt() происходят одновременно, уведомление может фактически потеряться. Если это может произойти и имеет драматические последствия, notifyAll() - более безопасный выбор, даже если вы платите за издержки (большую часть времени вы просыпаетесь слишком много потоков).

9 голосов
/ 21 мая 2010

Вот пример. Запустить его. Затем измените один из notifyAll () на notify () и посмотрите, что произойдет.

ProducerConsumerExample class

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) { }
        }
    }
}
9 голосов
/ 03 сентября 2008

От Джошуа Блоха, самого Гуру Явы в Эффективной Яве, 2-е издание:

«Пункт 69: Предпочитайте утилиты параллелизма ждать и уведомлять».

8 голосов
/ 25 октября 2015

Краткое резюме:

Всегда предпочитайте 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 () ), не вынимает элемент из очереди, поскольку он пуст, и notify () s - другой потребитель продолжить.

Напротив, notifyAll () пробуждает как производителей, так и потребителей. Выбор того, кто запланирован, зависит от планировщика. Конечно, в зависимости от реализации планировщика, планировщик может также только планировать потребителей (например, если вы назначаете потокам потребителей очень высокий приоритет). Однако в данном случае предполагается, что опасность планирования планировщика только для потребителей ниже, чем опасность того, что JVM только разбудит потребителей, потому что любой разумно реализованный планировщик не принимает просто произвольных решений. Скорее, большинство реализаций планировщика предпринимают какие-то усилия для предотвращения голодания.

5 голосов
/ 15 сентября 2012

Я очень удивлен, что никто не упомянул печально известную проблему «потерянного пробуждения» (Google google).

В основном:

  1. если у вас несколько потоков, ожидающих одного и того же условия, и
  2. несколько потоков, которые могут заставить вас перейти из состояния A в состояние B и,
  3. несколько потоков, которые могут заставить вас перейти из состояния B в состояние A (обычно те же потоки, что и в 1.) и,
  4. переход из состояния A в B должен уведомлять потоки в 1.

ТОГДА вы должны использовать notifyAll, если только у вас нет доказуемой гарантии невозможности потерянных пробуждений.

Типичным примером является параллельная очередь FIFO, где: несколько enqueuers (1. и 3. выше) могут перевести вашу очередь из пустой в непустую несколько dequeuers (2. выше) могут ждать условие "очередь не пуста" пусто -> непусто должно уведомлять декуайеров

Вы можете легко написать чередование операций, в которых, начиная с пустой очереди, взаимодействуют 2 энкейера и 2 декуейера, а 1 энкейер останется спящим.

Эта проблема, возможно, сопоставима с проблемой взаимоблокировки.

5 голосов
/ 29 мая 2014

Надеюсь, это прояснит некоторые сомнения.

notify () : метод notify () пробуждает один поток, ожидающий для блокировки (первый поток, который вызвал wait () для этой блокировки).

notifyAll () : метод notifyAll () пробуждает все потоки, ожидающие блокировки; JVM выбирает один из потоков из списка потоков, ожидающих блокировки, и просыпается что нить вверх.

В случае одного потока , ожидающего блокировки, между notify () и notifyAll () нет существенной разницы. Однако, когда существует более одного потока, ожидающего блокировки, и в notify (), и в notifyAll () точный пробужденный поток равен под управлением JVM , и вы не можете программно контролировать пробуждение конкретная тема.

На первый взгляд кажется, что хорошей идеей является просто вызвать notify () для пробуждения одного потока; может показаться ненужным разбудить все темы. Однако проблема с notify () состоит в том, что пробужденный поток может не подходить для пробуждения (поток может ожидать какого-то другого условия, или условие все еще не удовлетворено для этого нить и тд). В этом случае , notify () может быть потеряно, и никакой другой поток не проснется, что потенциально приведет к типу взаимоблокировки (уведомление потеряно, и все другие потоки ожидают уведомления - навсегда).

Чтобы избежать этой проблемы , всегда лучше вызывать notifyAll (), когда более одного потока ожидает блокировки (или более одного условия, при котором выполняется ожидание). Метод notifyAll () пробуждает все потоки, поэтому он не очень эффективен. однако эта потеря производительности незначительна в реальных приложениях.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...