Главная » Java, Советы » Никогда не вызывайте метод wait вне цикла

0

 

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

 

synchronized (obj) {

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

 obj.wait ();

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

 

}

 

При вызове метода 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. Результаты вызова всегда будут правильными, поскольку гарантируется, что вы разбудите все требуемые потоки. Заодно вы можете разбудить еще несколько других потоков, но это не повлияет на правильность вашей программы. Эти потоки проверят условие, которого они дожидаются, и, обнаружив, что оно не выполнено, продолжат ожидание.

 

Метод 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 нужно уделить особое внимание обеспечению живучести приложения.

 

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии