Теория и практика Java: Характеристика безопасности потока

Безопасность потока не является понятием типа "все-или-ничего"

В июле наш эксперт по параллельной обработке Брайан Гетц описывал классы Hashtable и Vector как "условно поточнобезопасные." Должен ли класс быть поточнобезопасным или нет? К сожалению, безопасноcть потока не является понятием типа "все-или-ничего", и ее удивительно трудно определить. Но, как объясняет Брайан в этой новой статье для раздела Теории и практики Java, исключительно важно попытаться систематизировать безопасность потока ваших классов в их Javadoc.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



26.02.2007

В прекрасной книге Джошуа Блоха (Joshua Bloch), Руководство по эффективному программированию на языке Java (см. Ресурсы), Раздел 52 озаглавлен "Безопасность потока документа," где разработчиков уговаривают точно документировать на понятном языке, какие даются гарантии безопасности потока для класса. Этот совет, как и большинство советов в книге Блоха, просто прекрасен, его часто повторяют, но гораздо реже применяют. (Как говорит Блох в своих беседах Программистские головоломки, "Не кодируйте как мой брат.")

Сколько раз вы искали класс в Javadoc, и задавались вопросом, "Является ли этот класс поточнобезопасным?" При отсутствии четкой документации читатели могут сделать неверные предположения о безопасности потока класса. Возможно, они считают его поточнобезопасным, хотя он таковым не является (а это очень плохо!), или они могут предположить, что его можно сделать поточнобезопасным путем синхронизирования объекта прежде, чем вызвать один из его методов (что может оказаться верным, но может и не дать эффекта, или в худшем случае, может создать иллюзию безопасности потока). В любом случае, лучше прояснить все в документации о поведении класса, если экземпляр находится в совместном доступе для нескольких потоков.

В качестве примера такой ловушки приведем класс java.text.SimpleDateFormat, который не является поточнобезопасным, но это не было указано до тех пор, пока в версии 1.4 JDK это не вошло в Javadoc. Сколько разработчиков ошибочно создали статический экземпляр SimpleDateFormat и использовали его из разных потоков, не зная, что их программы не будут вести себя должным образом в случае большой нагрузки? Никогда не делайте этого с вашими заказчиками или коллегами!

Запишите это, прежде чем позабудете (или уйдете работать в другую компанию)

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

Было бы хорошо, если бы безопасность потока была двоичным атрибутом класса, и можно было просто задокументировать, является ли класс поточнобезопасным или нет. Но, к сожалению, это не так просто. Если класс не является поточнобезопасным, можно ли сделать его поточнобезопасным, синхронизируя каждый доступ к объектам этого класса? Имеются ли последовательности операций, которые не допустят вмешательство других потоков, и, следовательно, потребуют синхронизировать не только основную операцию, но и все что касается составной операции? Существуют ли зависимости состояния между методами, требующими автоматического выполнения определенных групп операций? Именно такая информация необходима разработчикам при подготовке к использованию класса в параллельном приложении.


Определение безопасности потока

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

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

При таких определениях не удивительно, что безопасность потока сбивает нас с толку. Такие определения немногим лучше чем, высказывание о том, что "класс является поточнобезопасным, если он может быть безопасно вызван из различных потоков," что, конечно же, и имеется в виду, но не помогает нам отличить поточнобезопасный класс от небезопасного. Что мы понимаем под безопасностью?

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

Безопасность потока

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

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

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

Зависимости состояния между методами

Рассмотрим нижеприведенный фрагмент кода, который итерирует элементы Vector. Даже если все методы Vector синхронизированы, этот код все еще не будет безопасным для использования в многопоточной среде без дополнительной синхронизации, потому что если другой поток удаляет элемент в совершенно неподходящее время, get() может выдать исключение ArrayIndexOutOfBoundsException.

    Vector v = new Vector();

    // contains race conditions - may require external synchronization
    for (int i=0; i<v.size(); i++) {
      doSomething(v.get(i));
    }

Происходящее здесь говорит о том, что существует входное условие, являющееся частью спецификации get(index), где указано, что index должен быть неотрицательным и меньшим, чем size(). Но в многопоточной среде вы не можете проверить, является ли последнее измеренное значение size() все еще действительным и, следовательно, вы не можете знать величину i<size(), если только вы не выполняли исключающую блокировку с Vector еще до своего последнего вызова size().

Точнее, проблема возникает из-за того, что входное условие get() определено с точки зрения результата size(). Когда бы вы ни увидели подобный шаблон там, где вы должны использовать результаты одного метода для постановки условий ввода другого, у вас имеется зависимость состояния, и вы должны убедиться, что хотя бы этот элемент состояния не изменяется между вызовом двух методов. Как правило, единственным способом это сделать является выполнение исключающей блокировки объекта на все время раньше вызова первого метода, и до окончания вызова последнего; в вышеприведенном примере, где вы итерируете элементы Vector, вам может понадобиться синхронизация объекта Vector на время итерации.


Степени безопасности потока

Как показано в вышеприведенном примере, безопасность потока не происходит по типу "все-или-ничего". Все методы Vector являются синхронизированными, и Vector явно предназначен для функционирования в многопоточных средах. Но существуют ограничения его безопасности потока, а именно существуют зависимости состояния между определенными парами методов. (Аналогично, итераторы, возвращаемые Vector.iterator(), выдадут исключение ConcurrentModificationException, если Vector модифицирован другим потоком во время итерации.)

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

Блох представляет таксономию, которая описывает пять категорий безопасности потока: неизменяемая, поточнобезопасная, условно поточнобезопасная, поточно-совместимая и поточно-неблагоприятная. Не важно, используете ли вы эту систему или нет, если только вы четко документируете характеристики безопасности потока. Эта система имеет ограничения - границы между категориями не являются 100-процентно четкими, и существуют не описанные случаи - но эта система является неплохим началом. Главным в этой классификации является то, может (должен) ли или нет вызывающий окружать операции - или последовательности операций - внешней синхронизацией. Каждая из этих пяти категорий безопасности потока описана в следующих пунктах.

Неизменяемая

Постоянные читатели данной рубрики не удивятся, услышав, как я расхваливаю преимущества неизменяемости. Неизменяемые объекты являются гарантировано поточнобезопасными и никогда не требуют дополнительной синхронизации. Поскольку внешне различимое состояние неизменяемого объекта никогда не изменяется, если только он корректно сконструирован, то он не может находиться в несогласованном состоянии. Большинство классов исходных величин в библиотеках Java-классов, таких как Integer, String и BigInteger, являются неизменяемыми.

Поточно-безопасная

Поточно-безопасные объекты обладают свойствами, описанными выше в разделе "Безопасность потока" - чьи ограничения, заданные спецификацией класса, продолжают выполняться, когда произошел доступ к объекту из разных потоков, независимо от того, как потоки распределены средой выполнения, без какой-либо дополнительной синхронизации. Это надежная гарантия безопасности потока - многие классы, такие как Hashtable или Vector, не будут соответствовать такому строгому определению.

Условно поточнобезопасная

Мы обсуждали условную безопасность потока в июльской статье, "Параллельные классы коллекций." Условно поточнобезопасными классами являются такие классы, у которых каждая отдельная операция может быть поточнобезопасной, но определенные последовательности операций могут потребовать внешней синхронизации. Наиболее типичным примером условной безопасности потока является прохождение итератора, возвращенного из Hashtable или Vector - итераторы c быстрым прекращением (fail-fast), возвращенные данными классами, предполагают, что базовая коллекция не будет изменена во время прохождения итератора. Для того чтобы убедиться, что другие потоки не изменят соответствующую коллекцию во время прохождения, итерирующий поток должен быть уверен, что он имеет монопольный доступ к коллекции во время всего прохождения. Обычно монопольный доступ обеспечивается с помощью синхронизации блокировки - и в документации класса должно быть указано, какая это блокировка (обычно встроенный монитор объекта).

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

Поточно-совместимая

Поточно-совместимые классы не являются поточнобезопасными, но могут безопасно использоваться в параллельных средах с помощью соответствующей синхронизации. Это может означать окружение каждого вызова метода блоком synchronized блоком или создание упаковщика, где каждый метод является синхронизированным (как Collections.synchronizedList()). Или это может означать окружение определенных последовательностей операций блоком synchronized. Чтобы максимально повысить полезность поточно-совместимых классов, надо, чтобы они не требовали смнхронизации со стороны вызывающего конкретной блокировки, а просто чтобы во всех вызовах использовалась одна и та же блокировка. Выполнение этого позволит поточно-совместимым объектам работать в качестве переменных экземпляра в других поточнобезопасных объектах совмещения синхронизации имеющегося объекта.

Многие общие классы являются поточно-совместимыми, как, например, классы коллекций ArrayList и HashMap, java.text.SimpleDateFormat, или JDBC классы Connection и ResultSet.

Поточно-неблагоприятная

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

Другие факторы документации безопасности потока

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

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


Заключение

Поведение класса в отношении поточной безопасности является неотъемлемой частью его спецификации, и должно быть внесено в его документацию. Поскольку не существует декларативного способа описания поведения безопасности потока класса (пока), оно должно быть описано буквально. Хотя пятиуровневая система Блоха для описания степени потокобезопасности класса не охватывает все возможные случаи, она является прекрасной отправной точкой. Конечно, всем бы было лучше, если бы у каждого класса такая степень поведения потока входила в его Javadoc.

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=197957
ArticleTitle=Теория и практика Java: Характеристика безопасности потока
publish-date=02262007