Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


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



 

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

 

synchronized (obj) {

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

 obj.wait ();

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

 

}

 

 

188

 

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

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

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

 

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

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

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

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

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

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

Хотя в "The Java La n guage Speci f icatioп" [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 Noti f ication [Cargill96, Lea99]. Описание указанного шаблона выходит за рамки этой книги.

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

 

190

 

 

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

 

Не попадайте в зависимость от планировщика потоков

 

При выполнении в системе нескольких потоков соответствующий планировщик определяет, какие из них будут выполняться и в течение какого времени. Каждая правильная реализация JVМ пытается при этом добиться какой-то справедливости, однако конкретные стратегии диспетчеризации в различных реализациях сильно отличаются. Хорошо написанные многопоточные приложения не должны зависеть от особенностей этой стратегии. Любая программа, чья корректность или произ­водительность зависит от планировщика потоков, скорее всего окажется не переносимой.

Лучший способ написать устойчивую, гибкую и переносимую многопоточную программу - обеспечить условия, при которых в любой момент времени может вы­полняться несколько потоков. В этом случае планировщику потоков остается совсем небольшой выбор: он лишь передает управление выполняемым потокам, пока те еще могут выполняться. Как следствие, поведение программы не будет сильно меняться даже при выборе совершенно других алгоритмов диспетчеризации потоков.

Основной прием, позволяющий сократить количество запущенных потоков, за­ключается в том, что каждый поток должен выполнять небольшую порцию работы, а затем ждать наступления некоего условия (используя Object.wait) либо истечения не которого интервала времени (используя Thгead.sleep). Потоки не должны нахо­диться в состоянии активного ожидания (busy-wait), регулярно проверяя структуру данных и ожидая, пока с теми что-то произойдет. Помимо того, что программа при этом становится чувствительной к причудам планировщика, активное ожидание может значительно повысить нагрузку на процессор, соответственно уменьшая количество по­лезной работы, которую на той же машине могли бы выполнить остальные процессы.

Указанным рекомендациям отвечает пример с очередью заданий (статья 49): если предоставляемый клиентом метод pгocessItem имеет правильное поведение, то поток, обрабатывающий очередь, большую часть своего времени, пока очередь пуста, будет проводить в ожидании монитора. В качестве яркого примера того, как поступать не следует, рассмотрим еще одну неправильную реализацию класса WoгkQueue, в которой вместо работы с монитором используется активное ожидание:

 

//Ужасная программа: использует активное ожидание

// вместо метода Object . wait !

public abstract class WoгkQueue {

private final List queue = new LinkedList();

private boolean stopped = false;

 

191

 

 

import java.util.*;

public abstract class WorkQueue {

private final List queue = new LinkedList();

private boolean stopped = false;

protected WorkQueue() { new WorkerThread().start(); }

public final void enqueue(Object workItem) {

   synchronized (queue) { queue.add(workItem); }

}

public final void stop() {

   synchronized (queue) { stopped = true; }

}

protected abstract void processItem(Object workItem)

   throws InterruptedException;

private class WorkerThread extends Thread {

   public void run() {

       final Object QUEUE_IS_EMPTY = new Object();

       while (true) { // Главный цикл

           Object workItem = QUEUE_IS_EMPTY;

           synchronized (queue) {

               if (stopped)

                   return;

               if (!queue.isEmpty())

                   workItem = queue.remove(0);

           }

          if (workItem != QUEUE_IS_EMPTY) {

               try {

                   processItem(workItem);

               } catch (InterruptedException e) {

                   return;

               }

           }

       }

   }

}

}

 

Чтобы дать некоторое представление о цене, которую вам придется платить за такую реализацию, рассмотрим микротест, в котором создаются две очереди заданий и затем некое задание передается между ними в ту и другую сторону. (Запись о задании, передаваемая из одной очереди в другую,- это ссылка на первую очередь, которая служит адресом возврата.) Перед началом измерений программа выполняется десять секунд, 'чтобы система "разогрелась"; в течение следующих десяти секунд подсчитывается количество циклических переходов из очереди в очередь. На моей

 

 

192

 

машине окончательный вариант WorkQueue (статья 49) показал 23 000 циклических переходов в секунду, тогда как представленная выше некорректная реализация демон­стрирует 17 переходов в секунду.

 

class PingPongQueue extends WorkQueue {

volatile int count = 0;

 

protected void processItem(final Object sender) {

   count++;

   WorkQueue recipient = (WorkQueue) sender;

   recipient.enqueue(this);

}

}

 

 

public class WaitQueuePerf {

public static void main(String[] args) {

   PingPongQueue q1 = new PingPongQueue();

   PingPongQueue q2 = new PingPongQueue();

   q1.enqueue(q2); // Запускаем систему

 

   // Дадим системе 10 с на прогрев

   try {

         Thread.sleep(10000);

   } catch (InterruptedException e) {

   }

 

   // Подсчитаем количество переходов за 10 с

   int count = q1.count;

   try {

       Thread.sleep(10000);

   } catch (InterruptedException e) {

   }

   System.out.println(q1.count - count);

 

   q1.stop();

   q2.stop();

}

}

 

Приведенная реализация WorkQueue может показаться немного надуманной, тем не менее нередко можно встретить многопоточные системы, в которых безо всякой не­обходимости запускается один или несколько лишних потоков. И хотя результат этого может быть не таким экстремальным, как здесь продемонстрировано, производитель­ность и переносимость приложения, по-видимому, все же страдают.

 

193

 

 

Столкнувшись с тем, что программа едва работает из-за того, что некоторые потоки, по сравнению с остальными, не получают достаточно процессорного времени, не поддайтесь искушению  “исправить" программу, добавив в нее вызовы Thread.yield. Вы можете заставить программу работать, однако полученное прило­жение не будет переносимым с точки зрения производительности. Вызовы yield, улучшающие производительность в одной реализации JVM, в другой ее ухудшают, а в третьей не оказывают никакого влияния. У Thread.yield нет строгой семантики. Лучше измените структуру приложения таким образом, чтобы сократить количество параллельно выполняемых потоков.

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

Метод Thread.yield следует использовать для того, чтобы искусственно увеличить степень распараллеливания программы на время тестирования. Благо­даря просмотру большей части пространства состояний программы, это помогает; найти ошибки и удостовериться в правильности системы. Этот прием доказал свою высокую эффективность в выявлении скрытых ошибок многопоточной обработки.

Подведем итоги. Ваше приложение не должно зависеть от планировщика пото­ков. Иначе оно не будет ни устойчивым, ни переносимым. Как следствие, лучше не связывайтесь с методом Thread.yield и приоритетами. Эти функции предназначены единственно для планировщика. Их можно дозировано при менять для улучшения качества сервиса в уже работающей реализации, но ими нельзя пользоваться для "исправления" программы, которая едва работает.

 


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 215; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.039 с.)
Главная | Случайная страница | Обратная связь