Java: сповіщення () проти notifyAll () знову і знову


377

Якщо один Googles для "різниці між" notify()і notifyAll()", то з'явиться багато пояснень (залишивши осторонь пункти javadoc). Все це зводиться до кількості ниток очікування, що прокидаються: одна в notify()і все в notifyAll().

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

Яка корисна відмінність між notify () та notifyAll () тоді? Я щось пропускаю?


6
Корисні бібліотеки, які використовуються для одночасності, є в бібліотеках одночасності. Я вважаю, що це кращий вибір майже у кожному випадку. Бібліотека Concurency до дати Java 5.0 (в якій вони були додані як стандарт у 2004 році)
Пітер Лорі

4
Я не згоден з Петром. Бібліотека паралелізм реалізується в Java, і є багато Java коду виконується кожен раз , коли ви викликаєте блокування (), розблокування (), і т.д. Ви можете знімати себе в ногу, використовуючи паралелізм бібліотеку замість старого доброго synchronized, напевно , за винятком , досить рідкісні випадки використання.
Олександр Рижов

2
Ключовим непорозумінням, здається, є таке: ... для подальшого придбання монітора завжди вибирається лише одна нитка; у першому випадку - обраний В.М., у другому - той, який вибрав планувальник системних потоків. Мається на увазі, що по суті однакові. Незважаючи на те, що поведінка, як описано, є правильною, але цього не вистачає в тому, що у notifyAll()випадку, інші потоки після першого залишаються неспаними і отримують монітор, один за одним. У notifyвипадку жодна з інших ниток навіть не будить. Так функціонально вони дуже різні!
BeeOnRope

1) Якщо на об'єкті чекає багато потоків, а сповіщення () викликається лише один раз на цьому об'єкті. За винятком однієї з ниток очікування, що залишилися потоки чекають вічно? 2) Якщо сповіщення () використовується, тільки один з багатьох потоків очікування починає виконувати. Якщо використовується notifyall (), усі потоки, що очікують, отримують сповіщення, але лише одна з них починає виконувати, тож у чому тут використання notifyall ()?
Chetan Gowda

@ChetanGowda Повідомлення всіх потоків проти Повідомляти точно лише один довільний потік насправді має істотну різницю, поки ця, здавалося б, тонка, але важлива різниця не вразить нас. Коли ви повідомлите () лише 1 потік, усі інші потоки будуть у стані очікування, поки не отримає явне повідомлення / сигнал. Повідомляти все, все потоки будуть виконані і завершені в деякому порядку один за іншим без якого - або додаткового повідомлення - тут ми повинні сказати , що потоки blockedі не waiting.Коли blockedйого Exec буде тимчасово призупинено , доки інший потік не перебуває всередині syncблоку.
користувач104309

Відповіді:


248

Однак (якщо я правильно розумію різницю між цими методами), для подальшого придбання монітора завжди вибирається лише одна нитка.

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


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

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

Інший випадок, наприклад взаємовиключне блокування, лише одна з ниток очікування може зробити щось корисне після повідомлення (у цьому випадку придбати замок). У такому випадку ви скористаєтеся сповіщенням () . Правильно реалізований, ви можете використовувати notifyAll () і в цій ситуації, але ви зайво будете пробуджувати теми, які нічого не можуть зробити.


У багатьох випадках код для очікування умови буде записаний у циклі:

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

Таким чином, якщо o.notifyAll()виклик прокидає більше однієї чекаючої нитки, а перший, який повернувся з o.wait()макетів, залишає стан у помилковому стані, то інші потоки, які прокинулися, повернуться до очікування.


29
якщо ви повідомляєте лише про один потік, але кілька об'єктів очікують на об'єкт, як VM визначає, яку саме сповістити?
амфібій

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

15
Liedman сильно помиляється, специфікація Java прямо зазначає, що notify () не гарантується, що вона буде справедливою. тобто кожен виклик сповіщення може знову прокинути ту саму нитку (черга потоку в моніторі НЕ ПРАВИЛЬНА або FIFO). Однак планувальник гарантовано працює. Ось чому в більшості випадків, коли у вас є більше 2-х потоків, вам слід віддати перевагу notifyAll.
Yann TM

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петля у випадку, якщо ми отримаємо цю ситуацію:

Споживач 1 (C1) вводить синхронізований блок і буфер порожній, тому C1 вводиться в комплект очікування (через waitдзвінок). Споживач 2 (С2) збирається ввести синхронізований метод (у точці Y вище), але Виробник P1 ставить об'єкт у буфер і згодом викликає notify. Єдина нитка очікування - це C1, тому вона прокидається і тепер намагається знову придбати блокування об'єкта в точці X (вище).

Тепер C1 і C2 намагаються придбати замок синхронізації. Один з них (недетерміновано) вибирається та входить у метод, інший блокується (не чекає - але заблокується, намагаючись придбати блокування методу). Скажімо, С2 спочатку отримує замок. C1 все ще блокується (намагається придбати замок у X). C2 завершує метод і звільняє замок. Тепер, C1 набуває замок. Здогадайтесь, що, пощастило, у нас є whileцикл, оскільки, C1 виконує перевірку циклу (захист) і не дозволяє видалити неіснуючий елемент з буфера (C2 вже отримав його!). Якби у нас не було 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 char
- C2 намагається отримати 1 char - блоки при вході в getметод
- C3 намагається отримати 1 char - блоки при вході в getметод

КРОК 5:
- C1 виконує getметод - отримує таблицю char, викликає notify, виходить
- notifyПрокидається P2
- АЛЕ, C2 вводить метод до того, як P2 може (P2 повинен знову отримати замок), тому P2 блокує при вході в putметод
- C2 перевіряє цикл очікування, більше немає знаків у буфері, тому чекає
- C3 вводить метод після C2, але перед P2, перевіряє цикл очікування, більше немає знаків у буфері, тому чекає

КРОК 6:
- ЗАРАЗ: там чекають P3, C2 та C3!
- Нарешті, P2 отримує замок, ставить знак у буфер, викликає сповіщення, виходить із методу

КРОК 7:
- Повідомлення P2 прокидається P3 (пам’ятайте, що будь-яка нитка може бути пробуджена)
- P3 перевіряє стан циклу очікування, у буфері вже є знак, тому чекає.
- НЕ БІЛЬШЕ ТРЯДІВ, ЩО БУТИ ПОВІДОМЛЕННЯ, А ТРИ ТРЕТИ ВІДПОВІДНО!

РІШЕННЯ: Замінити notifyз notifyAllв коді виробника / споживача (вище).


1
finnw - P3 повинен повторно перевірити умову, оскільки notifyпричини P3 (обрана нитка в цьому прикладі) виходити з точки, яку вона чекала (тобто в whileциклі). Є й інші приклади, які не спричиняють тупикову ситуацію, проте в цьому випадку використання notifyне гарантує безвільний код. Використання notifyAllробить.
xagyg

4
@marcus Дуже близько. За допомогою notifyAll кожен потік повторно придбає замок (по одному), але зауважте, що після того, як одна нитка знову придбала замок і виконала метод (а потім вийшов) ... наступний потік знову отримує замок, перевіряє "поки" і повернеться до "очікування" (залежно від того, яка умова, звичайно). Отже, сповістіть прокидається один потік - як ви правильно заявляєте. notifyAll пробуджує всі потоки, і кожен потік заново блокує блокування по одному - перевіряє стан "while" і виконує метод або "очікує" знову.
xagyg

1
@xagyg, ти говориш про сценарій, в якому кожен виробник має зберігати лише одну загальну картину? Якщо так, то ваш приклад правильний, але не дуже цікавий ІМО. За допомогою додаткових кроків, які я пропоную, ви можете заткнути ту саму систему, але з необмеженою кількістю вводу - ось як зазвичай такі візерунки використовуються в реальності.
еран

3
@codeObserver Ви запитували: "Чи призведе до виклику notifyAll () одночасно, коли кілька потоків очікування перевіряють умову while () .. виняток? ". Ні, це неможливо, оскільки хоча декілька потоків прокидаються, вони не можуть одночасно перевірити стан часу. Кожен з них зобов'язаний повторно досягти блокування (відразу після очікування), перш ніж вони зможуть повторно увійти в розділ коду та повторно перевірити час. Тому по одному.
xagyg

4
@xagyg приємний приклад. Це поза темою від початкового запитання; просто заради обговорення. Тупик - це проблема дизайну imo (виправте мене, якщо я помиляюся). Тому що у вас є один замок, який поділяють і put, і get. І JVM недостатньо розумний, щоб дзвонити, поставлений після отримання звільнення блокування та віце-верша. Мертвий замок трапляється тому, що ставиться прокидається інший пут, який повертає себе в очікування () через час (). Чи працювали б два класи (і два замки)? Тож покладіть {synchonized (get)}, get {(synchonized (put)}. Іншими словами, get прокинеться лише поставити, а put буде прокинутися отримати лише.
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 () [...] прокидається один потік. Оскільки 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-- )

Уявіть, що у вас є додаток для споживачів, коли споживачі готові (тобто чекати () ) споживати, виробники готові (тобто чекати () ) виробити, а черга товарів (які виробляються / споживаються) порожня. У такому випадку сповіщення () може розбудити лише споживачів і ніколи не виробників, оскільки вибір, який прокинувся, є довільним . Споживчий цикл виробників не досяг би жодного прогресу, хоча виробники та споживачі готові відповідно виробляти та споживати. Натомість споживач прокидається (тобто залишає статус wait () ), не виймає товар з черги, оскільки він порожній, та повідомляє () іншого споживача про продовження.

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


9

Ось приклад. Виконати його. Потім змініть одне з notifyAll (), щоб сповістити (), і подивіться, що відбувається.

Клас 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 в Ефективній Java 2-го видання:

"Пункт 69: Віддайте перевагу комунальним програмам одночасного очікування та повідомлення".


16
Чому є більш важливим , ніж джерело.
Пейсьєр

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 () полягає в тому, що пробуджений потік може бути не підходящим для пробудження (потік може чекати якогось іншого стану, або умова все ще не виконується для цього потоку тощо). У цьому випадку сповіщення () може бути загублено, і жоден інший потік не прокинеться, що потенційно призведе до типу тупикової ситуації (сповіщення втрачено, а всі інші потоки чекають на сповіщення — назавжди).

Щоб уникнути цієї проблеми , завжди краще зателефонувати notifyAll (), коли на блокування очікується більше ніж один потік (або більше ніж одна умова, на яку очікування виконується). Метод notifyAll () прокидає всі потоки, тому він не дуже ефективний. однак ця втрата продуктивності незначна в реальних програмах.


6

Для потоку є три стани.

  1. ЗАЧЕКАЙТЕ - Потік не використовує жодного циклу процесора
  2. Блокований - Потік блокується, намагаючись придбати монітор. Він все ще може використовувати цикли процесора
  3. РУННІНГ - Нитка запущена.

Тепер, коли викликається сповіщення (), JVM вибирає один потік і переміщує їх у стан БЛОКОВАНО, а отже, у стан РОЗВ'ЯЗУВАННЯ, оскільки для об'єкта монітора немає конкуренції.

Коли викликається notifyAll (), JVM вибирає всі потоки та переміщує їх у стан BLOCKED. Усі ці потоки отримають блокування об'єкта за пріоритетом. Нитка, яка зможе спочатку придбати монітор, зможе спочатку перейти у стан RUNNING тощо.


Просто дивовижне пояснення.
royatirek

5

Я дуже здивований, що ніхто не згадував сумнозвісну проблему "загубленого пробудження" (google it).

В основному:

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

ТАКІ ви повинні використовувати notifyAll, якщо у вас немає підтверджених гарантій того, що втрачені пробудження неможливі.

Поширений приклад - паралельна черга FIFO, де: кілька запитувачів (1. і 3. вище) можуть переходити вашу чергу з порожнього на кілька порожніх декууерів (2. вище), можуть чекати умови "черга не порожня" порожня -> не порожній повинен сповіщати про декаутери

Ви можете легко написати переплетення операцій, в яких, починаючи з порожньої черги, взаємодіють 2 енкюкери і 2 декокери, а 1 енкевер залишиться спати.

Ця проблема, можливо, порівнянна з проблемою тупикового зв'язку.


Вибачте, 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 (), негайним результатом є те, що саме один інший потік придбає монітор і почне виконувати його. (Якщо припустити, що деякі потоки були фактично заблоковані на очікуванні () для цього об'єкта, інші пов'язані між собою потоки не просочують усі наявні ядра тощо).

Припустимо, потоки A, B і C чекали на цей об’єкт, а нитка A отримує монітор. Різниця полягає в тому, що відбувається, коли A відпустить монітор. Якщо ви використовували notify (), B і C все ще блокуються в режимі чекання (): вони не чекають на моніторі, вони чекають повідомлення. Коли A відпустить монітор, B і C все ще будуть сидіти там і чекати сповіщення ().

Якщо ви використовували notifyAll (), B і C просунулися через стан "чекати сповіщення" і обидва чекають придбання монітора. Коли A звільнить монітор, B або C придбають його (якщо припустимо, що інші потоки не змагаються за цей монітор) і розпочнуть його виконання.


Дуже чітке пояснення. Результат такої поведінки notify () може призвести до "пропущеного сигналу" / "пропущеного повідомлення", що призведе до тупикової ситуації / відсутності прогресу в стані програми. Виробник, C-споживач P1, P2 і C2 чекають C1. C1 дзвінки notify () і призначені для виробника, але C2 може бути пробуджено, тому P1 і P2 пропустили сповіщення, і я буду чекати подальшого явного "notification" (a notify () call).
користувач104309

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 на практиці:

Перший пункт, будь-ласка, повідомляти чи 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 з явним блокуванням блокування, наданим у 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) не дискримінує конкретний стан, якого чекають / повідомляють про / про.

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

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

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


0

Коли ви зателефонуєте на чекання () "об'єкта" (очікуючи, що блокування об'єкта придбано), інтерн звільнить замок на цьому об'єкті і допоможе іншим потокам заблокувати цей "об'єкт", у цьому сценарії буде більше 1 потоку очікують на "ресурс / об'єкт" (враховуючи, що інші потоки також видали очікування на той самий вище об'єкт і вниз, як буде нитка, яка заповнює ресурс / об'єкт і викликає сповіщення / notifyAll).

Тут, коли ви видаєте сповіщення про той самий об’єкт (з тієї ж / іншої сторони процесу / коду), це випустить заблокований та очікуючий єдиний потік (не всі потоки, що очікують - цей випущений потік буде обраний JVM Thread Планувальник і весь процес отримання блокування на об'єкті такий же, як і звичайний).

Якщо у вас є лише один потік, який буде обмінюватися / працювати над цим об’єктом, нормально використовувати метод notify () у вашій реалізації чекати-повідомляти.

якщо ви потрапляєте в ситуацію, коли більш ніж один потік читає і записує на ресурси / об’єкт, виходячи з вашої бізнес-логіки, тоді вам слід звернутися за notifyAll ()

тепер я дивлюся, як саме jvm ідентифікує та розриває очікування, коли ми видаємо сповіщення () на об’єкт ...


0

Хоча вище є тверді відповіді, я здивований кількістю прочитаних плутанин і непорозумінь. Це, мабуть, доводить думку про те, що потрібно якомога більше використовувати java.util.concurrent, а не намагатися написати власний розбитий паралельний код. Назад до питання: підводячи підсумок, найкраща сьогодні практика - це AVOID повідомляти () у ВСІХ ситуаціях через втрату проблеми з пробудженням. Кожен, хто цього не розуміє, не повинен дозволяти писати критичний код сумісності. Якщо ви турбуєтеся про проблему зі скотом, одним із безпечних способів домогтися пробудження однієї нитки одночасно є: 1. Створити чітку чергу очікування для ниток очікування; 2. Нехай кожна з ниток у черзі чекає попередника; 3. Повідомте кожен виклик потоку notifyAll (), коли буде завершено. Або ви можете використовувати Java.util.concurrent. *,


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

-2

Прокидання всього тут не має великого значення. зачекайте сповіщення та сповістіть усі, вони ставляться після володіння монітором об'єкта. Якщо потік знаходиться на етапі очікування і викликається повідомлення, ця нитка займе замок, і жодна інша нитка в цій точці не може зайняти цей замок. Тож паралельний доступ взагалі не може мати місце. Наскільки я знаю, будь-який дзвінок зачекати сповіщення та сповістити все можна лише після зняття блокування на об’єкт. Виправте мене, якщо я помиляюся.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.