Очевидно, 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
в коде производителя / потребителя (выше).