Главная страница
Навигация по странице:

  • // Создаем новый поток, который возвращает workltem в очередь Thread child = new Thread() { public void run() { enQueue(workltem); } };child. start(); child. join(); Взаимная блокировка!

  • // Чужой метод за пределами синхронизированного блока // "открытый вызов"

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

  • Ужасная программа использует активное ожидание // вместо метода Object.wait!

  • Effective Java tmprogramming Language GuideJ o s h u a b lo c h


    Скачать 1.05 Mb.
    НазваниеEffective Java tmprogramming Language GuideJ o s h u a b lo c h
    Дата03.04.2018
    Размер1.05 Mb.
    Формат файлаpdf
    Имя файлаBlokh_Dzh_-_Java_Effektivnoe_programmirovanie.pdf
    ТипДокументы
    #40178
    страница22 из 25
    1   ...   17   18   19   20   21   22   23   24   25

    // Блокировка
    } catch (InterruptedException е) { return; }
    }
    }
    }
    }
    }
    }
    184
    Чтобы воспользоваться этим классом, вы должны создать для него подкласс стем, чтобы предоставить реализацию абстрактного метода processItem. Например, следующий подкласс выводит на печать задания из очереди, не более одной записи в секунду и независимо оттого, с какой скоростью в очереди появляются новые задания class DisplayQueue extends WorkQueue { protected void processltem(Object workltem)
    throws InterruptedException {
    System,out.println(workltem);
    Thread.sleep(1000); }
    } Поскольку класс WorkQueue вызывает абстрактный метод риз синхронизированного блока, для него может наступить взаимная блокировка. Действительно, работа со следующим подклассом по описанной выше схеме приведет к взаимной блокировке DeadlockQueue extends WorkQueue { protected void processltem(final Object workltem)
    throws InterruptedException {
    // Создаем новый поток, который возвращает workltem в очередь Thread child = new Thread() { public void run() { enQueue(workltem); }
    };
    child. start(); child. join();
    Взаимная блокировка!

    }
    }
    Этот пример приведен потому, что здесь нет причин, которые заставили бы метод р создавать фоновый поток, однако описываемая проблема абсолютно реальна. Вызовы из синхронизированного блока методов, реализуемых за пределами класса, приводили к множеству взаимных блокировок в таких реальных системах, как программные пакеты с графическим интерфейсом пользователя. К счастью, эта проблема легко устранима. Вынесите вызов метода за пределы синхронизированного блока, как показано в следующем примере
    // Чужой метод за пределами синхронизированного блока
    // "открытый вызов"
    private class WorkerThread extends Thread { public void run() { while (true) {
    // Главный цикл
    Object workltem = null;
    185
    synchronized (queue) {
    try { while (queue.isEmpty()
    & & !stopped) queue.w ait();
    } catch (InterruptedException е) { return ; }
    if (stopped) return; workItem = queue. remove(0);
    } try {
    processItem(workItem
    ); // Блокировки нет
    } catch (InterruptedExcepti

    n return; }
    }
    }
    } Чужой метод, который вызывается за пределами синхронизированной области, называется открытым вызовом (open саll) [LeaOO,2.4.1.3]. Открытые вызовы не только предотвращают взаимную блокировку, но и значительно увеличивают распараллеливание вычислений. Если бы чужой метод вызывался из блокированной области и выполнялся сколь угодно долго, то все это время остальные потоки без всякой на то необходимости получали бы отказ в доступе к совместно используемому объекту. Правило таково, что в пределах синхронизированной области нужно выполнять как можно меньше работы заблокируйте ресурс, проверьте совместно используемые данные, преобразуйте их при необходимости и разблокируйте ресурс. Если требуется выполнение операций, отнимающих много времени, найдите способ вынести их за пределы синхронизированной области. Вызов чужого метода из синхронизированной области может привести к более серьезным сбоям, чем просто взаимная блокировка, если он происходит в тот момент, когда инварианты, защищаемые синхронизацией, временно недействительны. (В первом примере с очередью заданий этого случиться не может, поскольку при вызове метода р очередь находится в непротиворечивом состоянии) Возникновение таких сбоев не связано с созданием в чужом методе новых потоков. Это происходит, когда чужой метод делает обратный вызов пока что некорректного класса. И поскольку блокировка в языке программирования Java является рекурсивной, подобные обратные вызовы не приведут к взаимной блокировке, как это было бы, если бы вызовы производились из другого потока. Поток, из которого делается вызов, уже заблокировал область, и потому этот поток успешно пройдет через блокировку
    186
    во второй раз, несмотря на то, что над данными, защищенными блокировкой, в этот момент будет выполняться принципиально иная операция. Последствия такого сбоя могут быть катастрофическими - блокировка фактически не справилась со своей работой. Рекурсивные блокировки упрощают построение многопоточных объектно-ориентированных программ, однако они могут превратить нарушение живучести в нарушение безопасности. Мы обсудили проблемы параллельности потоков, теперь обратимся к производительности. Хотя с момента появления платформы Java расходы на синхронизацию резко сократились, полностью они не исчезнут никогда. И если часто выполняемую операцию синхронизировать без всякой необходимости, это может существенно сказаться на производительности приложения. Например, рассмотрим классы
    StringBuffer и BufferedInputStream. Эти классы имеют поддержку много поточности (статья 52), однако почти всегда их использует только один потока потому осуществляемая ими блокировка, как правило, оказывается избыточной. Они содержат методы, выполняющие тонкую обработку на уровне отдельных символов или байтов, а потому не только склонны выполнять ненужную работу по блокированию потоков, но имеют тенденцию использовать множество таких блокировок. Это может привести к значительному снижению производительности. Водной из статей сообщалось почти о 20% потерь для каждого реального приложения [Heydon99]. Вряд ли вы столкнетесь со столь существенным падением производительности, обусловленным излишней синхронизацией, однако 5-100/0 потерь вполне возможны. Можно утверждать, что все это относится к разряду "маленьких усовершенствований, о которых, как говорил Кнут, нам следует забыть (статья 37). Однако если выпишите абстракцию низкого уровня, которая в большинстве случаев будет работать с одним единственным потоком или как составная часть более крупного синхронизированного объекта, то следует подумать над тем, чтобы отказаться от внутренней синхронизации этого класса. Независимо оттого, будете вы синхронизировать класс или нет, крайне важно, чтобы в документации вы отразили его возможности при работе в многопоточном режиме (статья 52). Не всегда понятно, следует ли в указанном классе выполнять внутреннюю синхронизацию. Перечень, приведенный в статье 52, не дает четкого ответа на вопрос, следует ли поддерживать в классе МН020поточность или же его нужно сделать совместимым с многопоточностью. Приведем несколько рекомендаций, которые помогут вам в выборе. Если выпишите' класс, который будет интенсивно использоваться в условиях, требующих синхронизации, а также в условиях, когда синхронизация ненужна, правильный подход заключается в обеспечении обоих вариантов с синхронизацией (с поддержкой многопоточности - thread-safe) и без синхронизации (совместимый с многопоточностью - thread-соmраtibIе). Одно из возможных решений - создание класса-оболочки статья 14), в котором реализован соответствующий этому классу интерфейса перед передачей вызова внутреннего объекта соответствующему методу выполняется необходимая синхронизация. Такой подход применяется в Collections
    187

    Framework, а также в классе jауа.util.Random. Второй вариант решения, который можно использовать для классов, не предназначенных для расширения или повторной реализации, заключается в предоставлении класса без синхронизации, а также подкласса, состоящего исключительно из синхронизированных методов, которые вызывают соответствующие методы из суперкласса. Хорошим поводом для внутренней синхронизации класса является то, что он будет интенсивно использоваться параллельными потоками, а также то, что вы можете добиться большего параллелизма, выполняя тонкую внутреннюю синхронизацию. Например, можно реализовать хэш- таблицу постоянного размера, в которой доступ к каждому сегменту синхронизируется отдельно. Это обеспечивает более высокую степень распараллеливания, чем в случае блокирования всей таблицы при доступе к одному элементу. Если класс или статический метод связан с изменяемым статическим полем, он должен иметь внутреннюю синхронизацию, даже если обычно применяется только с одним потоком. В противоположность совместно используемому экземпляру, здесь клиент не имеет возможности произвести внешнюю синхронизацию, поскольку нет никакой гарантии, что другие клиенты будут делать тоже самое. Эту ситуацию иллюстрирует статический метод Math.random. Подведем итоги. Во избежание взаимной блокировки потоков и разрушения данных никогда не вызывайте чужие методы из синхронизированной области. Постарайтесь ограничить объем работы, выполняемой вами в синхронизированных областях. Проектируя изменяемый класс, подумайте о том, не должен ли он иметь свою собственную синхронизацию. Выигрыш, который вы рассчитываете получить, отказываясь от синхронизации, теперь уже не такой громадный, а вполне умеренный. Ваше решение должно исходить из того, будет ли ваша абстракция использоваться для работы с несколькими потоками. Четко документируйте свое решение. Никогда не вызывайте метод вне цикла bМетод Object.wait применяется в том случае, когда нужно заставить поток дождаться некоторого условия. Метод должен вызываться из синхронизированной области, блокирующей объект, для которого был сделан вызов. Стандартная схема использования метода wai t: synchronized (obj) { while условие не выполнено obj.wait ();
    // Выполнение действия, соответствующего условию
    }
    188
    При вызове метода w ait всегда используйте идиому цикла ожидания. Никогда не вызывайте его вне цикла. Цикл нужен для проверки соответствующего условия дои после ожидания. Проверка условия перед ожиданием и отказ от него, если условие уже выполнено, необходимы для обеспечения живучести. Если условие уже выполнено и перед переходом потока в состояние ожидания был вызван метод notify (или not1fyAll), нет никакой гарантии, что поток когда-нибудь выйдет из этого состояния. Проверка условия по завершении ожидания и вновь ожидание, если условие не выполнено, необходимы для обеспечения безопасности. Если поток производит операцию, когда условие не выполнено, он может нарушить инварианты, защищенные блокировкой. Существует несколько причин, по которым поток может "проснуться" при невыполненном условии За время от момента, когда поток вызывает метод notify, и до того момента, когда ожидающий поток проснется, другой поток может успеть заблокировать объект и поменять его защищенное состояние. Другой поток может случайно или умышленно вызвать метод notify, когда условие еще не выполнено. Классы подвергают себя такого рода неприятностям, если в общедоступных объектах присутствует ожидание. К этой проблеме восприимчив любой вызов wait в синхронизированном методе общедоступного объекта. Вовремя "побудки" потоков извещающий поток может вести себя слишком "щедро. Например, извещающий поток должен вызывать notifyAll, даже если условие пробуждения выполнено лишь для некоторых ожидающих потоков. Ожидающий поток может проснуться ив отсутствие извещения. Это называется ложным пробуждением (spurious wakeup). Хотя в "The Java Language п" [JLS] такая возможность не упоминается, во многих реализациях JVM применяются механизмы управления потоками, у которых ложные пробуждения хотя и редко, но случаются [Posix, 11.4.3.6.1]. Возникает еще один вопрос для пробуждения ОЖИ4ающих потоков следует использовать метод notify или notifyAll? (Напомним, что метод notify будит ровно один ожидающий поток в предположении, что такой поток существует, а notifyAll будит все ожидающие потоки) Часто говорится, что во всех случаях лучше применять метод notifyAll. Это разумный осторожный совет, который исходит из предположения, что все вызовы wait находятся в циклах while. Результаты вызова всегда будут правильными, поскольку гарантируется, что вы разбудите все требуемые потоки. Заодно вы можете разбудить еще несколько других потоков, но это не повлияет на правильность вашей программы. Эти потоки проверят условие, которого они дожидаются, и, обнаружив, что оно не выполнено, продолжат ожидание.
    189
    Метод notify можно выбрать в целях оптимизации, когда все потоки, находящиеся в состоянии ожидания, ждут одного итого же условия, однако при его выполнении в данный момент времени может пробудиться только один поток. Оба эти условия заведомо выполняются, если в каждом конкретном объекте в состоянии ожидания находится только один поток (как это было в примере из статьи 49). Но даже если эти условия справедливы, может потребоваться использование notifyAll. Точно также, как помещение вызова wait в цикл защищает общедоступный объект от случайных и злонамеренных извещений, применение notifyAll вместо notify защищает от случайного и злонамеренного ожидания в постороннем потоке. Иначе посторонний поток может "проглотить" важное извещение, оставив его действительного адресата в ожидании на неопределенное время. В примере г причина, по которой не использован метод notifyAll, заключается в том, что поток, обрабатывающий очередь, ждет своего условия в закрытом объекте (queue), а потому опасности случайных или злонамеренных ожиданий в других потоках здесь нет. Следует сделать Д предупреждение относительно использования notifyAll вместо notify. Хотя метод notifyAll не нарушает корректности приложения, он ухудшает его производительность для определенных структур данных производительность снижается с линейной до квадратичной зависимости от числа ждущих потоков. Это касается тех структур данных, при работе с которыми в любой момент времени вне котором особом состоянии находится определенное количество потоков, а остальные потоки должны ждать. Среди примеров таких структур семафоры т, буферы с ограничениями (bounded ЬиНег), а также блокировка чтения-записи (read-write lock). Если вы реализуете структуру данных подобного типа и будите каждый поток, только когда он становится приемлем для "особого статуса, то каждый поток вы будите один рази потребуется п операций пробуждения потоков. Если же вы будите все п потоков, то лишь один из них может получить особый статуса оставшиеся п -1 потоков возвращаются в состояние ожидания. К тому времени как все потоки из очереди ожидания получат особый статус, количество пробуждений составит n + п -1) + п -2) ... + 1. Сумма этого ряда равна Оп. Если вызнаете, что потоков всегда будет немного, проблем практически не возникают. Однако если такой уверенности нет, важно использовать более избирательную стратегию пробуждения потоков. Если все потоки, претендующие на получение особого статуса, логически эквивалентны, то все, что нужно сделать- это аккуратно использовать notify вместо notifyAll. Однако если в любой момент времени к получению особого статуса готовы лишь некоторые потоки из находящихся в состоянии ожидания, то вы должны применять шаблон, который называется Speci/ic Noti
    fication [Cargill96, Lea99]. Описание указанного шаблона выходит за рамки этой книги. Подведем итоги. Всегда вызывайте метод wait только из цикла, применяя стандартную идиому. Поступать иначе нет причин. Как правило, методу notify лучше
    190
    предпочитать noti fyAll. Однако в ряде ситуаций следование этому совету будет сопровождаться значительным падением производительности. При использовании notify нужно уделить особое внимание обеспечению живучести приложения. Не попадайте в зависимость от планировщика потоков При выполнении в системе нескольких потоков соответствующий планировщик определяет, какие из них будут выполняться ив течение какого времени. Каждая правильная реализация М пытается при этом добиться какой-то справедливости, однако конкретные стратегии диспетчеризации в различных реализациях сильно отличаются. Хорошо написанные многопоточные приложения не должны зависеть от особенностей этой стратегии. Любая программа, чья корректность или производительность зависит от планировщика потоков, скорее всего окажется непереносимой. Лучший способ написать устойчивую, гибкую и переносимую многопоточную программу - обеспечить условия, при которых в любой момент времени может выполняться несколько потоков. В этом случае планировщику потоков остается совсем небольшой выбор он лишь передает управление выполняемым потокам, пока те еще могут выполняться. Как следствие, поведение программы не будет сильно меняться даже при выборе совершенно других алгоритмов диспетчеризации потоков. Основной прием, позволяющий сократить количество запущенных потоков, заключается в том, что каждый поток должен выполнять небольшую порцию работы, а затем ждать наступления некоего условия (используя Object.wait) либо истечения некоторого интервала времени (используя г. Потоки не должны находиться в состоянии активного ожидания (busy-wait), регулярно проверяя структуру данных и ожидая, пока с теми что-то произойдет. Помимо того, что программа при этом становится чувствительной к причудам планировщика, активное ожидание может значительно повысить нагрузку на процессор, соответственно уменьшая количество полезной работы, которую на той же машине могли бы выполнить остальные процессы. Указанным рекомендациям отвечает пример с очередью заданий (статья 49): если предоставляемый клиентом метод г имеет правильное поведение, то поток, обрабатывающий очередь, большую часть своего времени, пока очередь пуста, будет проводить в ожидании монитора. В качестве яркого примера того, как поступать не следует, рассмотрим еще одну неправильную реализацию класса г, в которой вместо работы с монитором используется активное ожидание
    Ужасная программа использует активное ожидание
    // вместо метода Object.wait!
    public abstract class г { 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);
    1   ...   17   18   19   20   21   22   23   24   25


    написать администратору сайта