Очевидно, що 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
в коді виробника / споживача (вище).