Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 10 Потоки

// Tell timer we’re done

}

}

});

}

ready.await(); // Wait for all workers to be ready long startNanos = System.nanoTime(); start.countDown(); // And they’re off! done.await(); // Wait for all workers to finish return System.nanoTime() - startNanos;

}

Обратите внимание, что метод использует три защелки для под­ счета. Первая, ready, используется рабочими потоками, чтобы ска­ зать потоку таймера, когда он готов. Рабочие потоки затем ждут второй защелки, которой является start. Последний рабочий поток запускает ready. countDown, позволяя всем рабочим потокам про­ должать. Поток таймера ждет третьей защелки, done, пока послед­ ние рабочие потоки закончат выполнение действия и вызовут done. countDown. Как только это произойдет, поток таймера пробуждается и фиксирует время окончания. Стоит упомянуть еще несколько дета­ лей. Экзекутор, который передается методу time, должен позволить создание по крайней мере такого количества потоков, которое опре­ делено уровнем параллельности, иначе тест никогда не завершится. Это называется блокировка потока о т зависания [Goetz06, 8.1.1]. Если рабочий поток натолкнется на InterruptionException, он вы­ полняет прерывание, используя идиому Thread, currentThread(). in­ ter rupt(), и возвращает его метод run. Это позволяет экзекутору поступать с прерыванием так, как он считает нужным, что и должно быть. Наконец, обратите внимание, что System. nanoTime использует­ ся для записи времени деятельности чаще, чем System.currentTimeMillis. Для записи времени по интервалам всегда лучше использо­

вать System.nanoTime, чем System. currentTimeMillis. System.nanoTime

и более аккуратен и более точен, а также на него не влияют настройки системного времени.

380

Цикл служит для про­

С тать я 69

Эта статья лишь поверхностно освещает утилиты параллельности. Например, три защелки счетчика из предыдущего примера могут за­ меняться одним цикличным барьером. Получающийся код даже более краткий, но его труднее понять. Для более детальной информации см. Java Concurrency in Practice [Goetz06]. Вам всегда следует предпо­ читать использовать утилиты параллельности вместо wait и notify. Метод wait используется, чтобы заставить поток ждать наступления определенных условий. Он должен запускаться в синхронизирован­ ной области, которая запирает объект, на котором он запускается. Вот стандартная идиома для использования метода wait:

// Стандартная идиома, использующая метод wait synchronized (obj) {

while (^condition does not hold>)

obj.wait(); // (Releases lock, and reacquires on wakeup)

... // Perform action appropriate to condition

}

Всегда используйте идиому цикла wait для запуска метода wait; никогда не запускайте ее вне цикла.

верки условий до и после ожидания.

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

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

Другой поток получил блокировку и изменил защищенное состо­ яние в период между временем, когда поток запустил notify, и вре­ менем пробуждения ожидающего потока.

381

Глава 10 Потоки

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

Уведомляющий поток может быть чересчур щедрым на пробуж­ дение ожидающих потоков. Например, поток уведомления может запустить notifуА11, даже если только некоторые из ожидающих по­ токов удовлетворяют условиям.

Ожидающий поток может (правда, редко) проснуться при от­ сутствии уведомления. Это известно под названием ложное пробуж­ дение [Posix, 11.4.3.6.1: JavaSE6].

Связанный вопрос, нужно ли использовать notify или not ifуА11 для пробуждения ожидающих потоков. (Вспомните, что notify про­ буждает один ожидающий поток, принимая во внимание, что поток существует и notifyAll будит все ожидающие потоки.) Часто сове­ туют использовать notifyAll. Это разумный консервативный совет. Он всегда выдаст корректный результат, потому что гарантировано, что вы разбудите потоки, которые должны быть разбужены. Вы мо­ жете также разбудить некоторые другие потоки, но это не повлияет на правильность выполнения программы. Эти потоки проверят усло­ вия, для которых они проснулись, и, обнаружив, что они не соответ­ ствуют им, останутся в режиме ожидания.

В качестве оптимизации вы можете выбрать запуск notify вме­ сто notifyAll, если все потоки, которые могут быть в наборе ожида­ ния, ждут одного и того же условия и только один поток в одно время может извлечь выгоду от того, что условие будет истиной.

Даже если эти условия окажутся истиной, могут быть другие причины для использования notifyAll вместо notify. Так же как помещение запуска wait в цикл защищает от случайных и злонаме­ ренных уведомлений на общедоступном объекте, использование no­ tifyAll вместо notify защищает от случайных или злонамеренных ожиданий несвязанного потока. Такие ожидания могут проглотить

382

С тать я 70

критические уведомления, оставив их потенциальных получателей в неопределенном ожидании.

Подведем итоги. Использование методов wait и notify непосред­ ственно — это как программирование на языке ассемблер по сравнению с высокоуровневым программированием, предоставленным java.utils. concurrent. Редко, если вообще когда-либо, бывает причина исполь­ зовать wait и notify в новом коде. Если вы поддерживаете код, содер­ жащий wait и notify, убедитесь, что wait всегда запускается из цикла while, используя стандартную идиому. Метод notifyAll должен в об­ щем и целом использоваться предпочтительнее, чем notify. Если ис­ пользуется notify, необходимо серьезно позаботиться о его живучести.

При работе с потоками документируйте уровень безопасности

То, как класс работает, когда его экземпляры и статические мето­ ды одновременно используются в нескольких потоках, является важ­ ной частью соглашений, устанавливаемых классом для своих клиентов. Если вы не отразите эту сторону поведения класса в документации, использующие его программисты будут вынуждены делать допуще­ ния. И если эти допущения окажутся неверными, полученная програм­ ма может иметь либо недостаточную (статья 66), либо избыточную (статья 67) синхронизацию. В любом случае это способно привести к серьезным ошибкам.

Иногда говорится, что пользователи могут сами определить, безопасен ли метод при работе с несколькими потоками, если про­ верят, присутствует ли модификатор synchronized в документации, генерируемой утилитой Javadoc. Это неверно по нескольким причи­ нам. Хотя в ранних версиях утилита Javadoc действительно указы­ вала в создаваемом документе модификатор synchronized, это было ошибкой, и в версии 1.2 она была устранена. Наличие в декларации метода модификатора synchronized —это деталь реализации, а не

383

Глава 10 Потоки

часть внешнего API. Присутствие модификатора не является на­ дежной гарантией того, что метод безопасен при работе с нескольки­ ми потоками. От версии к версии ситуация может меняться.

Более того, само утверждение о том, что наличия ключевого сло­ ва synchronized достаточно для того, чтобы говорить о безопасности при работе с несколькими потоками, содержит в себе распространен­ ную микроконцепцию о категоричности этого свойства. На самом деле класс может иметь несколько уровней безопасности. Чтобы класс можно было безопасно использовать в среде со многими потоками, в документации к нему должно быть четко указано, какой уровень безопасности он поддерживает.

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

Неизменяемый (immutable). Экземпляры такого класса вы­

глядят для своих клиентов как константы. Никакой внешней синхронизации не требуется. Примерами являются String,

Integer и Biglnteger (статья 13).

• С поддержкой многопоточности (thread-safe). Экземпляры такого класса могут изменяться, однако все методы имеют до­ вольно надежную внутреннюю синхронизацию, чтобы эти эк­ земпляры могли параллельно использовать несколько потоков безо всякой внешней синхронизации. Параллельные вызовы будут обрабатываться последовательно в некотором глобально согласованном порядке. Примеры: Random и java.util.Time г.

С условной поддержкой многопоточности (conditionally thread-safe). То же, что и с поддержкой многопоточности, за исключением того, что класс (или ассоциированный

384

С татья 70

класс) содержит методы, которые должны вызываться один за другим без взаимного влияния со стороны других пото­ ков. Для исключения возможности такого влияния клиент должен установить соответствующую блокировку на время выполнения этой последовательности. Примеры: Hashtable

и Vector, чьи итераторы требуют внешней синхронизации.

Не поддерживающий многопоточность (not thread-safe) — экземпляры такого класса изменяемы, и их можно безопасно

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

такие как ArrayList и HashMap.

Несовместимый с многопоточностью (thread-hostile). Этот класс небезопасен при параллельной работе с несколь­ кими потоками, даже если вызовы всех методов окружены внешней синхронизацией. Обычно несовместимость связана

стем обстоятельством, что эти методы меняют некие статиче­ ские данные, которые оказывают влияние на другие потоки. К счастью, в библиотеках платформы Java лишь очень немно­ гие классы и методы несовместимы с многопоточностью. Так,

метод System. runFinalizersOnExit несовместим с многопо­

точностью и признан устаревшим.

Эти категории (кроме несовместимого с многопоточностью) соот­ ветствуют примерно аннотациям по безопасности потоков (thread safety annotation), приведенными в книге Java Concurrency in Practice,

которыми являются Immutabe, ThreadSafe и NotThreadSafe [Goetz06,

Appendix А]. Поддерживающие и условно поддерживающие много­ поточность категории описываются в аннотации ThreadSafe.

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

385

Collections.synch ronizedMap.

Глава 10 Потоки

и какую блокировку (в редких случаях — блокировки) необходимо поставить, чтобы исключить одновременный доступ. Обычно это блокировка самого экземпляра, но не всегда. Если объект является альтернативным представлением какого-либо другого объекта, кли­ ент должен получить блокировку для основного объекта с тем, чтобы воспрепятствовать его непосредственной модификации со стороны других потоков. Например, в документации к методу Collection, synchronizedMap говорится следующее:

Обязательно следует использовать ручную синхронизацию на воз­ вращаемой схеме при итерации любого из представлений коллекции:

Мар<К, V> m = Collections.synchronizedMap(new HashMapcK, V>());

Set<K> s = m.keySet(); // Needn’t be in synchronized block

synchronized(m) { // Synchronizing on m, not s! for (K key : s)

key.f();

}

Если не последовать этому совету, то может наблюдаться неде­ терминистическое поведение.

Описание безопасности потока класса обычно содержится в ком­ ментариях к документации на него, но методы с особыми настройками безопасности потока должны описывать эти свойства в своих собствен­ ных комментариях к документации. Нет необходимости документи­ ровать неизменяемость перечислимых типов, если только не понятно по возвращаемым типам. Методы статической генерации должны до­ кументировать безопасность потоков на возвращаемом объекте, как продемонстрировано на примере выше с

Если класс начнет использовать политику блокировки открытого доступа, он дает клиентам возможность выполнять последовательный запуск методов атомарно, но это стоит гибкости. Это недопустимо при высокопроизводительном внутреннем контроле параллельности

такого вида, как ConcurrentHashMap и ConcurrentLinkedQueue. Клиент

386

С тать я 70

также может вызвать D O S -атаку, удерживая блокировку открытого доступа длительное время. Это может произойти случайно либо на­ меренно.

Для предотвращения такой D O S -атаки вы можете использовать

закрытую блокировку объекта (private lock object) вместо исполь­ зования синхронизированных методов (которые используют блок

соткрытым доступом):

//Идиома закрытой блокировки объекта - препятствует DOS-атаке private final Object lock = new ObjectQ;

public void foo() { synchronized(lock) {

}

}

Поскольку закрытая блокировка объекта недоступна клиентам класса, то они не могут вмешиваться в процесс синхронизации объек­ та. Для эффекта мы применим совет из статьи 13, инкапсулируя бло­ кированный объект в рамках объекта, который он синхронизирует.

Обратите внимание, что поле lock объявляется как final. Это не позволяет вам по неосторожности изменить его содержимое, что может привести к катастрофическим последствиям, в частности к несинхронизированному доступу к содержащемуся объекту (ста­ тья 66). Мы применим совет из статьи 15, сводя к минимуму изме­ няемость поля lock.

Для повторной итерации идиома закрытого объекта блокировки может использоваться только в классах с безусловной поддержкой многопоточности. Классы с условной поддержкой многопоточности не могут использовать идиому, потому что им надо сначала докумен­ тировать, какую именно блокировку должны получить их клиенты при выполнении определенной последовательности запуска методов.

Использование внутренних объектов для блокировки особенно подходит классам, которые предназначены для наследования (ста­ тья 17) . Если бы такой класс должен был использовать экземпля­

387

Глава 10 Потоки

ры для блокировки, то подкласс мог бы легко и непреднамеренно вмешаться в операции основного класса и наоборот. Используя одну и ту же блокировку для разных целей, суперкласс и подкласс стали бы в конце концов «наступать друг другу на пятки». Это не теоретиче­ ская проблема. Например, это происходит с классом Thread [Bloch05, задача 77].

Подведем итоги. Для каждого класса необходимо четко докумен­ тировать возможность работы с несколькими потоками. Единственная возможность сделать это — представить аккуратно составленный текст описания. К описанию того, как класс поддерживает многопоточность, наличие модификатора synchronized отношения не имеет. Однако для классов с условной поддержкой многопоточности в документации важно указывать, какой объект следует заблокировать, чтобы после­ довательность обращений к методам стала неделимой. Если вы пишете класс с безусловной поддержкой потоков, то рассмотрите использова­ ние закрытой блокировки вместо синхронизированных методов. Это защищает вас от вмешательства в синхронизацию клиентов, подклас­ сов и дает вам гибкость в применении более сложного подхода к кон­ тролю параллельности в последующих версиях.

С осторожностью используйте отложенную инициализаию

Отложенная инициализация — это задержка инициализации поля до тех пор, пока значение не потребуется. Если значение не по­ требуется вовсе, поле никогда не инициализируется. Этот прием при­ меняется как на статических полях, так и на полях экземпляров. Хотя в основном отложенная инициализация является оптимизацией, она также может разрушить вредоносную зацикленность при инициали­ зации класса или экземпляра [Bloch05 , задача 51].

Как и в случае с большинством оптимизаций, лучшим советом для отложенной инициализации будет «не используйте ее до тех пор,

388

С тать я 71

пока она вам действительно не понадобится» (статья 55). Отложен­ ная инициализация — это палка о двух концах. Она уменьшает затра­ ты на инициализацию класса или создание экземпляра за счет увели­ чения затрат на доступ к полю, инициализация которого отложена. В зависимости от того, какая часть инициализируемого поля в конце концов требует инициализации, насколько затратной будет их ини­ циализация и как часто будет производиться доступ к каждому полю, отложенная инициализация может (как и любая оптимизация) по­ вредить производительности.

Как уже говорилось, отложенная инициализация имеет свою область применения. Если к полю получает доступ только часть эк­ земпляра класса and и инициализация поля требует затрат, тогда, возможно, стоит использовать отложенную инициализацию. Един­ ственный способ узнать наверняка — измерить производительность класса с или без отложенной инициализации.

При наличии нескольких потоков отложенная инициализация может быть довольно сложна. Если два или более потока используют общее инициализируемое поле, то важно, чтобы применялась какая -либо форма синхронизации, иначе могут быть ужаснейшие ошибки (статья 66). Все приемы инициализации, обсуждаемые в этой ста­ тье, поддерживают многопоточность.

В большинстве случаев нормальная инициализация более пред­ почтительна, чем отложенная. Вот типичный пример декларирования нормально инициализируемого поля экземпляра. Обратите внимание на использование модификатора final (статья 15):

// Нормальная инициализация экземпляра поля

private final FieldType field = computeFieldValue();

Е сли вы используете отложенную инициализацию для прерыва­ ния зацикленности инициализации, используйте синхронизирован­ ный метод доступа, так как он является наиболее простой и понятной альтернативой:

//Отложенная инициализация поля экземпляра - синхронизированный

//метод доступа

389

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