Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Effective Java TM.doc
Скачиваний:
3
Добавлен:
28.09.2019
Размер:
2.11 Mб
Скачать

Никогда не вызывайте метод wait вне цикла

Метод Object.wait применяется в том случае, когда нужно заставить поток дождаться некоторого условия. Метод должен вызываться из синхронизированной области, блокирующей объект, для которого был сделан вызов. Стандартная схема использования метода wai t:

synchronized (obj) {

while (<условие не выполнено>)

obj.wait ();

// Выполнение действия, соответствующего условию

}

188

При вызове метода wait всегда используйте идиому цикла ожидания. Никог­да не вызывайте его вне цикла. Цикл нужен для проверки соответствующего условия до и после ожидания.

Проверка условия перед ожиданием и отказ от него, если условие уже выполнено, необходимы для обеспечения живучести. Если условие уже выполнено и перед пере­ходом потока в состояние ожидания был вызван метод notify (или not1fyAll), нет никакой гарантии, что поток когда-нибудь выйдет из этого состояния.

Проверка условия по завершении ожидания и вновь ожидание, если условие не выполнено, необходимы для обеспечения безопасности. Если поток производит опе­рацию, когда условие не выполнено, он может нарушить инварианты, защищенные блокировкой. Существует несколько причин, по которым поток может "проснуться" при невыполненном условии:

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

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

  • Во время "побудки" потоков извещающий поток может вести себя слишком "щедро". Например, извещающий поток должен вызывать notifyAll, даже если условие пробуждения выполнено лишь

для некоторых ожидающих потоков.

  • Ожидающий поток может проснуться и в отсутствие извещения.

Это называется ложным пробуждением (spurious wakeup).

Хотя в "The Java Language Specificatioп" [JLS] такая возможность не упоминается, во многих реализациях JVM применяются механизмы управления потоками, у которых ложные пробуждения хотя и редко, но случаются [Posix, 11.4.3.6.1].

Возникает еще один вопрос: для пробуждения ОЖИ4ающих потоков следует ис­пользовать метод notify или notifyAll? (Напомним, что метод notify будит ровно один ожидающий поток в предположении, что такой поток существует, а notifyAll будит все ожидающие потоки.) Часто говорится, что во всех случаях лучше применять метод notifyAll. Это разумный осторожный совет, который исходит из предполо­жения, что все вызовы wait находятся в циклах while. Результаты вызова всегда будут правильными, поскольку гарантируется, что вы разбудите все требуемые пото­ки. Заодно вы можете разбудить еще несколько других потоков, но это не повлияет на правильность вашей программы. Эти потоки проверят условие, которого они дожи­даются, и, обнаружив, что оно не выполнено, продолжат ожидание.

189

Метод notify можно выбрать в целях оптимизации, когда все потоки, находя­щиеся в состоянии ожидания, ждут одного и того же условия, однако при его выпол­нении в данный момент времени может пробудиться только один поток. Оба эти условия заведомо выполняются, если в каждом конкретном объекте в состоянии ожидания находится только один поток (как это было в при мере WorkQueue из статьи 49).

Но даже если эти условия справедливы, может потребоваться использование notifyAll. Точно так же, как помещение вызова wait в цикл защищает общедоступ­ный объект от случайных и злонамеренных извещений, применение notifyAll вместо notify защищает от случайного и злонамеренного ожидания в постороннем потоке. Иначе посторонний поток может "проглотить" важное извещение, оставив его дейст­вительного адресата в ожидании на неопределенное время. В примере WoгkQueue при­чина, по которой не использован метод notifyAll, заключается в том, что поток, обрабатывающий очередь, ждет своего условия в закрытом объекте (queue), а потому опасности случайных или злонамеренных ожиданий в других потоках здесь нет.

Следует сделать ~ДHO предупреждение относительно использования notifyAll вместо notify. Хотя метод notifyAll не нарушает корректности приложения, он ухудшает его производительность: для определенных структур данных производитель­ность снижается с линейной до квадратичной зависимости от числа ждущих потоков. Это касается тех структур данных, при работе с которыми в любой момент времени в не котором особом состоянии находится определенное количество потоков, а осталь­ные потоки должны ждать. Среди примеров таких структур: семафоры (seтaphore), буферы с ограничениями (bounded ЬиНег), а также блокировка чтения-записи (read-write lock).

Если вы реализуете структуру данных подобного типа и будите каждый поток, только когда он становится приемлем для "особого статуса", то каждый поток вы будите один раз и потребуется п операций пробуждения потоков. Если же вы будите все п потоков, то лишь один из них может получить особый статус, а оставшиеся п -1 потоков возвращаются в состояние ожидания. К тому времени как все потоки из очереди ожидания получат особый статус, количество пробуждений составит n + (п -1) + (п -2) ... + 1. Сумма этого ряда равна О(п2). Если вы знаете, что потоков всегда будет немного, проблем практически не возникают. Однако если такой уверенности нет, важно использовать более избирательную стратегию пробуждения потоков.

Если все потоки, претендующие на получение особого статуса, логически экви­валентны, то все, что нужно сделать,- это аккуратно использовать notify вместо notifyAll. Однако если в любой момент времени к получению особого статуса готовы лишь некоторые потоки из находящихся в состоянии ожидания, то вы должны приме­нять шаблон, который называется Speci/ic Notification [Cargill96, Lea99]. Описание указанного шаблона выходит за рамки этой книги.

Подведем итоги. Всегда вызывайте метод wait только из цикла, применяя стан­дартную идиому. Поступать иначе нет причин. Как правило, методу notify лучше

190

предпочитать noti fyAll. Однако в ряде ситуаций следование этому совету будет сопровождаться значительным падением производительности. При использова­нии notify нужно уделить особое внимание обеспечению живучести приложения.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]