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

  • // Ошибка требуется синхронизация

  • // Нормальная статическая инициализация (неотложенная)

  • 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
    страница21 из 25
    1   ...   17   18   19   20   21   22   23   24   25

    // Пустой блок catch игнорирует исключение - крайне подозрительный коде Пустой блок catch лишает исключение смысла, который состоит в том, чтобы вы обрабатывали исключительную ситуацию. Игнорировать исключение это все равно, что игнорировать пожарную тревогу выключить сирену, чтобы больше ни у кого не было возможности узнать, есть ли здесь настоящий пожар. Либо вам удастся Всех обмануть, либо результаты окажутся катастрофическими. Когда бы вы ни увидели пустой блок catch, в вашей голове должна включаться сирена. Блок catch обязан содержать, по крайней мере,
    комментарий, объясняющий, почему данное исключение следует игнорировать. Ситуацию, когда игнорирование исключений может оказаться целесообразным,
    Иллюстрирует такой пример как визуализация изображений в мультипликации. Если экран обновляется через равные промежутки времени, то, возможно, лучший способ Справиться с временным сбоем - игнорировать его и подождать следующего обновления экрана.
    175
    Представленная в этой статье рекомендация в равной степени относится как к обрабатываемым, таки к необрабатываемым исключениям. Вне зависимости оттого, представляет ли исключение предсказуемое условие или программную ошибку, если оно игнорируется и используется пустой блок catch, тов результате программа, столкнувшись с ошибкой, будет работать дальше, никак на нее не реагируя. Затем в любой произвольный момент времени программа может завершиться с ошибкой, и программный код, где это произойдет, не будет иметь никакого отношения к действительному источнику проблемы. Должным образом обработав исключение, вы можете избежать отказа. Даже простая передача необрабатываемого исключения вовне вызовет, по крайней мере, быстрый останов программы, при котором будет сохранена информация, полезная при устранении сбоя.
    176
    Глава 9
    Потоки
    Потоки позволяют выполнять одновременно несколько операций в пределах одной программы.
    Многопоточное программирование сложнее однопоточного, так что совет из статьи 30 здесь особенно актуален если существует библиотечный класс, который может оградить вас от многопоточного программирования низкого уровня, во чтобы тони стало воспользуйтесь им. Одним из примеров таких классов является java.util.Тiтeг. Второй пример- пакет util.concurrent Дага Ли (Doug Lea) [Lea01], содержащий целый набор утилит высокого уровня для управления потоками. Но, несмотря на наличие библиотек, вам все равно время от времени приходится писать или поддерживать программный код для многопоточной обработки. В этой главе содержатся советы, которые помогут вам создавать понятные, правильные и хорошо документированные программы для работы с потоками.
    С и н х ро низ и р у й те доступ потоков к совместно используемыми з меняемым данным Использование ключевого слова synchronized дает гарантию, что в данный момент времени некий оператор или блок будет выполняться только водном потоке. Многие программисты рассматривают синхронизацию лишь как средство блокировки потоков, которое не позволяет одному потоку наблюдать объект в промежуточном состоянии, пока тот модифицируется другим потоком. С этой точки зрения, объект создается с согласованным состоянием (статья 13), а затем блокируется методами, имеющими к нему доступ. Эти методы следят за состоянием объекта и (дополнительно) могут вызывать для него переход состояния (state transition), переводя объект из одного согласованного состояния в другое. Правильное выполнение синхронизации гарантирует, что ни один метод никогда не сможет наблюдать этот объект в промежуточном состоянии.
    177
    Такая точка зрения верна, ноне отражает всей картины. Синхронизация не только запрещает потоку наблюдать объект в промежуточном состоянии, она также дает гарантию, что объект будет переходить из одного согласованного состояния в другое в результате выполнения четкой последовательности шагов. Каждый поток, попадая в синхронизированный метод или блок, видит результаты выполнения всех предыдущих переходов под управлением того же самого кода блокировки. После того как поток покинет синхронизированную область, любой поток, попадающий в область синхронизированную с помощью той же блокировки, увидит результат перехода в новое состояние, осуществленного предыдущим потоком (если переход имел место. Язык Java гарантирует, что чтение и запись отдельной переменной, если это не переменная типа long или double, являются атомарными операциями. Иными словами, чтение переменной (кроме long и double) будет возвращать значение, которое было записано в эту переменную одним из потоков, даже если несколько потоков без какой-либо синхронизации одновременно записывают новые значения в эту переменную. Возможно, вы слышали, что для повышения производительности при чтении и записи атомарных данных нужно избегать синхронизации. Это неправильный совет с опасными последствиями. Хотя свойство атомарности гарантирует, что при чтении атомарных данных поток не увидит случайного значения, нет гарантии, что значение, записанное одним потоком, будет увидено другим синхронизация необходима как для блокирования потоков, таки для надежного взаимодействия между ними. Это является следствием сугубо технического аспекта языка программирования Java, который называется моделью памяти (тетогу m odel) [JLS, 17]. Вероятно, в ближайшей версии модель памяти будет существенно пересмотрена [Pugh01a], однако описанная особенность скорее всего не поменяется. Отсутствие синхронизации для доступа к совместно используемой переменной может иметь серьезные последствия, даже если переменная имеет свойство атомарности как при чтении, таки при записи. Рассмотрим следующую функцию генерации серийного номера
    // Ошибка требуется синхронизация private static п пехtSегiаlNumЬег = о ; public static int generateSerialNumber() геtuгп nextSerialNumber+
    +; Эта функция должна гарантировать, что при каждом вызове метода generateSerialNumber будет возвращаться другой серийный номер до тех пор, пока не будет произведено вызова. Для защиты инвариантов данного генератора серийных номеров синхронизация ненужна, поскольку таковых у него нет. Состояние генератора содержит лишь одно атомарно записываемое поле (пехtSегiаlNumЬег), для которого допустимы любые значения. Тем не менее без синхронизации этот метод не работает. Оператор приращения (++) осуществляет чтение и запись в поле пехtSегiаlNumЬег, а потому атомарным не является. Чтение и запись - независимые операции, которые
    выполняются последовательно. Несколько параллельных потоков могут наблюдать в поле nextSerialNumber одно и тоже значение и возвращать один и тот же серийный номер. Еще более удивительный случай один поток может несколько раз вызвать метод generateSerialNumber и получить последовательность серийных номеров от 0 до n. После этого другой поток может вызвать метод generateSerialNumber и получить серийный номер, равный нулю. Без синхронизации второй поток может не увидеть ни одного из изменений, произведенных первым потоком. Это следствие применения вышеупомянутой модели памяти. Исправление метода generateSerialNumber сводится к простому добавлению в его декларацию слова sупсhгопizеd. Тем самым гарантируется, что различные вызовы не будут смешиваться, и каждый новый вызов будет видеть результат обработки всех предыдущих обращений. Чтобы сделать этот метод "железобетонным, возможно, имеет смысл заменить int на long или инициировать какое-либо исключение, если nextSerialNumber будет близко к переполнению. Рассмотрим процедуру остановки потока. Платформа Java предлагает методы принудительной остановки потока, но они являются устаревшими и по своей сути небезопасны работа сними может привести к разрушению объектов. Для остановки потока рекомендуется использовать прием, заключающийся в том, что в классе потока создается некое опрашиваемое поле, которому можно присвоить новое значение, указывающее на то, что этот поток должен остановить себя сам. Обычно такое поле имеет тип Ьооlеаn или является ссылкой на объект. Поскольку чтение и запись этого поля атомарный, у некоторых программистов появляется соблазн предоставить к нему доступ без синхронизации. Нередко можно увидеть программный код такого рода
    // Ошибка требуется синхронизация
    public class StoppableThread extends Thread { private bооlеаn stopRequested = false; public void run() { boolean dоnе = false; while (!stopRequested & &
    !dоnе) {
    // Здесь выполняется необходимая обработка
    }
    }
    public void requestStop() {
    stopRequested = true; Проблема приведенного кода заключается в том, что в отсутствие синхронизации нет гарантии если ее вообще можно дать, что поток, подлежащий остановке, "увидит, что другой поток поменял значение stopRequested. В результате метод requestStop
    179
    может оказаться абсолютно неэффективным. И хотя вы вряд ли действительно столкнетесь со странным поведением программы, пока не запустите ее в многопроцессорной, системе, гарантировать ее правильную работу нельзя. Разрешить эту проблему можно, непосредственно синхронизировав любой доступ к полю stopRequested:
    // Правильно синхронизированное совместное завершение потока public class StoppableThread extends Thread { private boolean stopRequested = false; public void гип() { boolean done = false; while (!stopRequested() & & !done) {
    // Здесь выполняется необходимая обработка }
    }
    public synchronized void requestStop() { stopRequested = true; }
    private synchronized boolean stopRequested() {
    return stopRequested; Заметим, что выполнение каждого из синхронизированных методов является атомарным синхронизация используется исключительно для обеспечения взаимодействия потоков, а не для блокировки. Очевидно, что исправленный программный код работает, а расходы на синхронизацию при каждом прохождении цикла вряд ли можно заметить. Однако есть корректная альтернатива, которая не столь многословна, и ее ПРОИЗВ6Дительность чуть выше. Синхронизацию можно опустить, если объявить stopRequested с модификатором volatile (асинхронно-изменяемый). Этот модификатор гарантирует, что любой поток, который будет читать это поле, увидит самое последнее записанное значение. Наказание за отсутствие в предыдущем примере синхронизации доступа к полю stopRequested оказывается сравнительно небольшим результат вызова метода requestStop может проявиться через неопределенно долгое время. Наказание за отсутствие синхронизации доступа к изменяемым, совместно используемым данным может быть более суровым. Рассмотрим идиому двойной проверки

    (double-check) отложенной инициализации
    // Двойная проверка отложенной инициализации – неправильная static Foo foo = null;
    180
    public static Foo getFoo() { if (foo == null) { synchronized (Foo.class) if (foo == null) foo = new Foo(); }
    }
    return foo; Идея, на которой построена эта идиома, заключается в том, чтобы избежать затратна синхронизацию доступа к уже инициализированному полю foo. Синхронизация используется здесь только для того, чтобы не позволить сразу нескольким потокам инициализировать данное поле. Идиома дает гарантию, что поле будет инициализировано не более одного раза и что все потоки, вызывающие метод getFoo, будут получать правильную ссылку на объект. К сожалению, это не гарантирует, что ссылка на объект будет работать правильно. Если поток прочел ссылку на объект без синхронизации, а затем вызывает в этом объекте какой-либо метод, может оказаться, что метод обнаружит свой объект в частично инициализированном состоянии, а это приведет к катастрофическому сбою программы. То, что поток может видеть объект с отложенным созданием в частично инициализированном состоянии, кажется диким. Объект был полностью собран прежде, чем его ссылка была "опубликована" в поле (foo), откуда ее получат остальные потоки. Однако в отсутствие синхронизации чтение "опубликованной" ссылки на объект еще не дает гарантии, что соответствующий поток увидит все те данные, которые были записаны в память перед публикацией ссылки на объект. В частности, нет гарантии того, что поток, читающий опубликованную ссылку на объект, увидит самые последние значения данных, составляющих внутреннюю структуру этого объекта. Вообще говоря, идиома двойной проверки неработоспособна, хотя она и может действовать, если переменная, совместно используемая разными потоками, содержит простое значение, а не ссылку на объект [Pugh01b]. Решить эту проблему можно несколькими способами. Простейший из них - полностью отказаться от отложенной инициализации
    // Нормальная статическая инициализация (неотложенная)
    private static final Foo foo = new Foo(); public static Foo getFoo() {
    return foo; Этот вариант, безусловно, работает, и метод getFoo оказывается настолько быстр, насколько это возможно. Здесь нет ни синхронизации, ни каких-либо еще вычислений. Как говорилось В статье 37, вы должны писать простые, понятные, правильные
    181
    программы, оставляя оптимизацию на последний момент. Приступать к оптимизации следует только тогда, когда измерения покажут, что она необходима. Поэтому отказ от отложенной инициализации, как правило, оказывается наилучшим решением. Если вы отказались от отложенной инициализации, измерили расход ресурсов и обнаружили, что он чрезмерно высок, то необходимо должным образом синхронизировать метод для выполнения отложенной инициализации
    // Правильно синхронизированная отложенная инициализация private static Foo foo = null; public static synchronized Foo getFoo() { if (foo == null) foo = new Foo(); return foo; Этот метод работает, но при каждом вызове теряется время на синхронизацию. для современных реализаций jVM эти потери сравнительно невелики. Однако если, измеряя производительность вашей системы, вы обнаружили, что не можете себе позволить ни обычную инициализацию, ни синхронизацию каждого доступа, есть еще один вариант. Идиому класса, 8ыполняющеzо инициализацию по запросу

    (initializeon-demand holder class), лучше применять в том случае, когда инициализация статического поля, занимающая много ресурсов, может и не потребоваться, однако если уж поле понадобилось, оно используется очень интенсивно. Указанная идиома представлена ниже
    // Идиома класса, выполняющего, инициализацию по запросу private static class FooHolder { static final Foo foo = new Foo(); }
    public static Foo getFoo() { return FooHolder. foo; Преимуществом этой идиомы является гарантия того, что класс не будет инициализироваться до той поры, пока он не потребуется [jLS, 12.4.1]. При первом вызове метод getFoo читает поле FooHolder. foo, заставляя класс FooHolder выполнить инициализацию. Красота идиомы заключается в том, что метод getFoo не синхронизирован и всего лишь предоставляет доступ к полю foo, так что отложенная инициализация практически не увеличивает издержек доступа. Единственным недостатком этой идиомы является то, что она не работает с экземплярами полей, а только со статическими полями класса. Подведем итоги. Когда несколько потоков совместно работают с изменяемыми данными, каждый поток, который читает или записывает эти данные, должен пользоваться блокировкой. Пусть гарантии, связанные с атомарностью чтения и записи, не удерживают вас от выполнения правильной синхронизации. Без
    182
    синхронизации невозможно гарантировать, что изменения в объекте, сделанные одним потоком, будут увидены другим. Несинхронизированный доступ к данным может привести к отказам, затрагивающим живучесть и безопасность системы. Воспроизвести такие отказы бывает крайне сложно. Они могут зависеть от времени и чрезвычайно чувствительны к деталям реализации jVM и к особенностям компьютера. При некоторых условиях использование модификатора volatile представляет собой реальную альтернативу обычной синхронизации, однако это пока новаторский прием. Более того, границы его применимости станут известны лишь по завершении ведущихся ныне работ над моделью памяти. Избегайте избыточной синхронизации Статья 48 предупреждает об опасностях недостаточной синхронизации. Данная статья посвящена обратной проблеме. В зависимости от ситуации избыточная синхронизация может приводить к снижению производительности приложения, взаимной блокировке потоков или даже к непредсказуемому поведению программы. для исключения возможности взаимной блокировки (deadlock) никогда не передавайте управление клиенту, если находитесь в синхронизированном методе или блоке. Иными словами, из области синхронизации не следует вызывать открытые или защищенные методы, которые предназначены для переопределения. (Такие методы обычно являются абстрактными, но иногда по умолчанию могут иметь определенную реализацию) Сточки зрения класса, содержащего синхронизированную область, такой метод является чужим. У класса нет сведений о том, что этот метод делает, нет над ним контроля. Клиент может реализовать этот метод таким образом, чтобы он создавал другой поток, выполняющий обратный вызов этого же класса. Затем вновь созданный поток может попытаться получить доступ к области, блокированной первым потоком, что приведет к блокировке нового потока. И если метод, создавший новый поток, ждет его завершения, возникает взаимная блокировка. для пояснения рассмотрим класс, в котором реализована очередь заданий (work queue). Этот класс позволяет клиентам ставить задания в очередь на асинхронную обработку. Метод enqueue может вызываться столь часто, сколь это необходимо. Конструктор класса запускает фоновый поток, который удаляет из очереди записи в том порядке, в котором они были сделаны, и обрабатывает их, используя метод processItem. Если очередь заданий больше ненужна, клиент вызывает метод stop, чтобы заставить поток изящно остановиться после завершения всех заданий, находящихся в обработке. public abstract class WorkQueue { private final List queue = new LinkedList(); private boolean stopped = false;
    183
    protected WorkQueue() { new WorkerThread().start(); }
    public final void enqueue(Object workltem) {
    synchronized (queue) { queue.add(worklt

    m); queue.notify(); }
    }
    public final void stop() { synchronized (queue) { stopped = true; queue. notify(); }
    }
    protected abstract void processltem(Object workltem) throws InterruptedException;
    // Ошибка вызов чужого метода из синхронизированного блока class WorkerThread extends Thread { public void run() {
    while (true) { Главный цикле workltem = queue. геmоvе(0); try { processltem(workltem):
    1   ...   17   18   19   20   21   22   23   24   25


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