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