Java: notify () против notifyAll () снова и снова


377

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

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

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


6
Полезные библиотеки для параллелизма находятся в библиотеках параллелизма. Я полагаю, что это лучший выбор почти в каждом случае. Библиотека Concurency до Java 5.0 (в которой они были добавлены в качестве стандарта в 2004 году)
Питер Лори

4
Я не согласен с Питером. Библиотека параллелизма реализована в Java, и каждый раз, когда вы вызываете lock (), unlock () и т. Д., Выполняется много Java-кода. Вы можете выстрелить себе в ногу, используя библиотеку параллелизма вместо старого synchronized, за исключением некоторых случаев. , довольно редкие случаи использования.
Александр Рыжов

2
Основное недоразумение, по-видимому, заключается в следующем: ... всегда выбирается только один поток для дальнейшего получения данных монитора; в первом случае тот, который выбран виртуальной машиной, во втором случае тот, который выбран планировщиком системного потока. Подразумевается, что они по сути одинаковы. Хотя описанное поведение является правильным, в данном notifyAll()случае отсутствует то, что в этом случае _ другие потоки после первого остаются активными и получают монитор один за другим. В этом notifyслучае ни один из других потоков даже не проснулся. Так что функционально они очень разные!
BeeOnRope

1) Если много объектов ожидают объекта, и notify () вызывается только один раз для этого объекта. За исключением одного из ожидающих потоков, остальные потоки ждут вечно? 2) Если используется notify (), запускается только один из множества ожидающих потоков. Если используется notifyall (), все ожидающие потоки уведомляются, но только один из них начинает выполняться, так что здесь используется notifyall ()?
Четан Говда,

@ChetanGowda Уведомление всех потоков против Уведомления ровно только у одного произвольного потока на самом деле есть существенная разница, пока это, казалось бы, неуловимое, но важное различие поражает нас. Когда вы уведомляете () только 1 поток, все остальные потоки будут в состоянии ожидания, пока он не получит явное уведомление /сигнал. Уведомляя всех, все потоки будут выполняться и завершаться в некотором порядке один за другим без какого-либо дополнительного уведомления - здесь мы должны сказать, что потоки - это blockedне так. waitingКогда blockedего exec временно приостановлен, пока другой поток не окажется внутри syncблока.
user104309

Ответы:


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
если вы уведомляете только один поток, но несколько ожидают объекта, как виртуальная машина определяет, какой из них следует уведомить?
амфибия

6
Я не могу сказать наверняка о спецификации Java, но в целом вы должны избегать предположений о таких деталях. Я думаю, вы можете предположить, что виртуальная машина будет делать это в здравом уме и, в основном, честно.
Лиедман

15
Лидман совершенно не прав, спецификация Java явно заявляет, что notify () не гарантируется быть честным. т. е. каждый вызов с уведомлением может снова вызывать один и тот же поток (очередь потоков на мониторе НЕ ЯВЛЯЕТСЯ ФАРИТОМ или FIFO). Однако планировщик гарантированно будет честным. Именно поэтому в большинстве случаев, когда у вас более 2 потоков, вы должны предпочесть notifyAll.
Янн ТМ

45
@YannTM Я за конструктивную критику, но я думаю, что ваш тон немного несправедлив. Я прямо сказал «не могу сказать наверняка» и «я думаю». Расслабься, ты когда-нибудь писал что-то семь лет назад, что не на 100% правильно?
Лиедман

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

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

СЕЙЧАС ЖЕ,

Хорошо, теперь почему мы должны уведомить все?

В приведенном выше примере производителя / потребителя это выглядит так, как будто мы можем сойти с рук notify. Кажется, это так, потому что мы можем доказать, что защита в циклах ожидания для производителя и потребителя является взаимоисключающей. То есть, похоже, что у нас не может быть потока, ожидающего как в 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:
- C1 пытается получить 1 символьный
блок - C2 пытается получить 1 символьный блок при входе в 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в коде производителя / потребителя (выше).


1
finnw - P3 должен перепроверить условие, потому что notifyP3 (выбранный поток в этом примере) следует из точки, которую он ожидал (то есть внутри whileцикла). Существуют и другие примеры, которые не вызывают взаимоблокировки, однако в этом случае использование notifyне гарантирует код без взаимоблокировок. Использование notifyAllделает.
xagyg

4
@marcus Очень близко. С помощью notifyAll каждый поток будет повторно получать блокировку (по одному за раз), но обратите внимание, что после того, как один поток повторно получил блокировку и выполнил метод (и затем вышел) ... следующий поток повторно получает блокировку, проверяет «while» и вернусь к «ждать» (в зависимости от состояния, конечно). Итак, оповещение будит одним потоком - как вы правильно заявили. notifyAll пробуждает все потоки, и каждый поток восстанавливает блокировку по одному - проверяет состояние «while» и либо выполняет метод, либо «ждет» снова.
xagyg

1
@xagyg, вы говорите о сценарии, в котором у каждого производителя есть только один символ для хранения? Если это так, ваш пример правильный, но не очень интересный IMO. С помощью дополнительных шагов, которые я предлагаю, вы можете заблокировать ту же самую систему, но с неограниченным количеством входных данных - именно так такие шаблоны обычно используются в реальности.
Эран

3
@codeObserver Вы спросили: «Не вызовет ли вызов notifyAll () многократный ожидающий поток, одновременно проверяющий условие while () .. и, следовательно, существует вероятность того, что до того, как некоторое время будет удовлетворено, 2 потока уже вышли из него, вызывая outOfBound исключение? Нет, это невозможно, поскольку, хотя несколько потоков просыпаются, они не могут одновременно проверять состояние while. Каждый из них обязан повторно установить блокировку (сразу после ожидания), прежде чем они смогут повторно войти в раздел кода и повторно проверить время. Поэтому по одному.
xagyg

4
@xagyg хороший пример. Это не по теме из оригинального вопроса; просто ради обсуждения. Тупик - это проблема дизайна imo (поправьте меня, если я ошибаюсь). Потому что у вас есть одна блокировка, совместно используемая как put, так и get. И JVM не достаточно умна, чтобы вызвать put после того, как get выпустит блокировку и наоборот. Мёртвая блокировка происходит, потому что put вызывает другой пут, который возвращает себя в wait () из-за while (). Будет ли работать два класса (и два замка)? Так что 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.

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


10

Краткое содержание:

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

Объяснение:

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

источник: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

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

Таким образом, если у вас нет приложения, в котором огромное количество потоков выполняет одно и то же одновременно, предпочтение notifyAll () перед notify () . Почему? Потому что, как другие пользователи уже ответили на этом форуме, уведомить ()

пробуждает единственный поток, который ожидает на мониторе этого объекта. [...] Выбор является произвольным и происходит на усмотрение реализации.

источник: Java SE8 API ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify-- )

Представьте, что у вас есть приложение потребителя производителя, в котором потребители готовы (то есть, ожидают () ) потреблять, производители готовы (то есть, ждать () ), чтобы произвести, и очередь товаров (которые будут произведены / потреблены) пуста. В этом случае 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 в Effective Java 2nd edition:

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


16
Почему является более важным , чем источник.
Pacerier

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. WAIT - поток не использует цикл процессора
  2. BLOCKED - поток блокируется при попытке получить монитор. Возможно, он по-прежнему использует циклы процессора
  3. RUNNING - поток запущен.

Теперь, когда вызывается notify (), JVM выбирает один поток и переводит его в состояние BLOCKED и, следовательно, в состояние RUNNING, поскольку нет конкуренции за объект монитора.

Когда вызывается notifyAll (), JVM выбирает все потоки и переводит их в состояние BLOCKED. Все эти потоки получат блокировку объекта в приоритетном порядке. Поток, который может получить монитор первым, сможет сначала перейти в состояние RUNNING и так далее.


Просто потрясающее объяснение.
Роятирек

5

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

В принципе:

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

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

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

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

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


Мои извинения, xagyg объясняет это подробно. Название проблемы - «потерянный пробуждение»
NickV

@Abhay Bansal: я думаю, что вы упускаете тот факт, что condition.wait () освобождает блокировку, и она вновь получает поток, который пробуждается.
NickV

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 получает монитор. Разница заключается в том, что происходит, когда А выпускает монитор. Если вы использовали notify (), то B и C по-прежнему блокируются в wait (): они не ждут на мониторе, они ожидают уведомления. Когда A отпускает монитор, B и C все еще сидят там, ожидая уведомления ().

Если вы использовали notifyAll (), то B и C оба вышли за состояние «ожидания уведомления» и оба ожидают получения монитора. Когда A освобождает монитор, B или C получат его (при условии, что другие потоки не конкурируют за этот монитор) и начнут выполнение.


Очень четкое объяснение. Результатом такого поведения notify () может стать «Пропущенный сигнал» / «Пропущенное уведомление», что приведет к взаимоблокировке / отсутствию ситуации с состоянием приложения. P-Producer, C-Consumer 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 на практике:

Первый пункт, будь то Notify или NotifyAll?

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

Если два потока A и B ожидают в разных предикатах условия одной и той же очереди условий и вызывается уведомление, то дело до JVM, которому JVM потока сообщит.

Теперь, если уведомление предназначено для потока A, а JVM уведомляет поток B, тогда поток B проснется и увидит, что это уведомление бесполезно, поэтому он будет ждать снова. И поток А никогда не узнает об этом пропущенном сигнале, и кто-то похитил его уведомление.

Таким образом, вызов notifyAll решит эту проблему, но, опять же, он будет влиять на производительность, поскольку он будет уведомлять все потоки, и все потоки будут бороться за одну и ту же блокировку, и это будет включать переключение контекста и, следовательно, загрузку ЦП. Но мы должны заботиться о производительности только в том случае, если она ведет себя правильно, если ее поведение не является правильным, тогда производительность бесполезна.

Эта проблема может быть решена с помощью объекта Condition явной блокировки Lock, представленного в jdk 5, поскольку он обеспечивает различное ожидание для каждого предиката условия. Здесь он будет вести себя правильно и не будет проблем с производительностью, поскольку вызовет сигнал и убедится, что только один поток ожидает этого условия.


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 пробуждает все потоки, а затем все потоки борются за блокировку этого объекта. Теперь планировщик ЦП выбирает поток, который получает блокировку для этого объекта. Этот процесс отбора будет намного лучше, чем отбор JVM. Таким образом, обеспечение жизни.

2

Посмотрите на код, размещенный @xagyg.

Предположу , что два разных потоков ждут два различных условий: первая нить ждет , а второй поток ждет .
buf.size() != MAX_SIZEbuf.size() != 0

Предположим, что в какой-то момент buf.size() не равно 0 . JVM вызывает notify()вместо notifyAll(), и первый поток уведомляется (не второй).

Первый поток просыпается, проверяет, buf.size()может ли он вернуться MAX_SIZE, и возвращается к ожиданию. Второй поток не проснулся, продолжает ждать и не звонит get().


1

notify()пробуждает первый поток, который вызвал wait()на тот же объект.

notifyAll()пробуждает все потоки, вызвавшие один и wait()тот же объект.

Поток с наивысшим приоритетом будет запущен первым.


13
В случае, если notify()это не совсем « первый поток ».
Bhesh Gurung

6
Вы не можете предсказать, какой из них будет выбран VM. Только Бог знает.
Сергей Шевчик

Нет гарантии, кто будет первым (без справедливости)
Иван

Первый поток проснется только в том случае, если ОС это гарантирует, и, скорее всего, этого не произойдет. Это действительно зависит от операционной системы (и ее планировщика), чтобы определить, какой поток следует активировать.
Пол Стелиан

1

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


1

Подводя итог прекрасным подробным объяснениям, приведенным выше, и самым простым способом, который я могу себе представить, это связано с ограничениями встроенного монитора JVM, который 1) приобретается для всего блока синхронизации (блока или объекта) и 2) не делает различий в отношении конкретного условия, ожидающего / уведомленного о / о.

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

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

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


0

Когда вы вызываете wait () для «объекта» (ожидая, что блокировка объекта получена), интерн это снимет блокировку с этого объекта и поможет другим потокам заблокировать этот «объект», в этом сценарии будет более чем один поток ожидает «ресурс / объект» (учитывая, что другие потоки также выдавали ожидание для того же вышеупомянутого объекта, и вниз по пути будет поток, который заполняет ресурс / объект и вызывает notify / notifyAll).

Здесь, когда вы отправляете уведомление об одном и том же объекте (с той же / другой стороны процесса / кода), это освобождает заблокированный и ожидающий один поток (не все ожидающие потоки - этот освобожденный поток будет выбран потоком JVM). Планировщик и весь процесс получения блокировки на объекте такой же, как и обычный).

Если у вас есть только один поток, который будет совместно использовать этот объект, то в вашей реализации wait-notify можно использовать только метод notify ().

если вы находитесь в ситуации, когда более одного потока читает и записывает ресурсы / объекты на основе вашей бизнес-логики, тогда вам следует обратиться к notifyAll ()

теперь я смотрю, как именно jvm идентифицирует и прерывает ожидающий поток, когда мы запускаем notify () для объекта ...


0

Хотя выше есть несколько убедительных ответов, я удивлен тем количеством недоразумений и недоразумений, которые я прочитал. Это, вероятно, подтверждает идею о том, что следует как можно больше использовать java.util.concurrent вместо того, чтобы пытаться писать собственный неработающий параллельный код. Вернемся к вопросу: подведем итог: сегодня лучше избегать уведомления () во всех ситуациях из-за проблемы с пробуждением. Любой, кто не понимает этого, не должен иметь права писать критически важный код параллелизма. Если вы беспокоитесь о проблеме скотоводства, то один из безопасных способов добиться пробуждения одного потока за раз: 1. Создать явную очередь ожидания для ожидающих потоков; 2. Пусть каждый поток в очереди ожидает своего предшественника; 3. Пусть каждый поток вызывает notifyAll () после завершения. Или вы можете использовать Java.util.concurrent. *,


по моему опыту, использование wait / notify часто используется в механизмах очереди, где поток ( Runnableреализация) обрабатывает содержимое очереди. wait()Затем используется всякий раз , когда очередь пуста. И notify()вызывается при добавлении информации. -> в таком случае есть только 1 поток, который когда-либо вызывает wait(), тогда не выглядит немного глупо использовать a, notifyAll()если вы знаете, что есть только 1 ожидающий поток.
bvdb

-2

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.