Effective Java: Concurrency

Напоминаю, что данная серия постов — кратчайшее изложение основных мыслей книги Effective Java 2nd edition. Они не заменят книги, но помогут освежить кое-что в памяти.


Item 66. Синхронизируйте доступ к общим (shared) изменяемым (mutable) данным.

Параграф о применении ключевых слов synchronized и volatile. Автор делает следующие выводы: лучше всего избегать межпоточной синхронизации использованием immutable объектов. Если это невозможно, всегда нужна синхронизация, иначе результат работы программы будет непредсказуем. В случаях, когда блокировки (ключевое слово synchronized) не нужны, допустимо использовать volatile. Но и с ним не всё так просто — надо помнить, что не все операции атомарны (++ и -- не атомарны, операции чтения и записи для long и double тоже).


Item 67. Избегайте избыточной синхронизации.

Никогда не запускайте «чужой» (alien) код внутри синхронизированных блоков. «Чужим» называется код, предоставляемый клиентом вашего API. Соответственно, вы не знаете, что и как будет выполняться в этом коде, поэтому в некоторых случаях выполнение «чужого» кода может привести к дедлокам (deadlocks), состоянию гонки (race condition) ошибкам выполнения. Автор приводит пример кода с такой проблемой. Выполнение «чужого» кода нужно выносить за пределы synchronized-блоков. Если это невозможно, синхронизацию следует переложить на клиентский код, при этом явно задокументировав потоковую небезопасность вашего API.

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

Если ваш класс не предполагает обязательной многопоточности, лучше вообще ничего не синхронизовывать — пусть лучше клиент об этом заботится. Это правило нарушено во многих старых стандартных классах, например, в StringBuffer. Он синхронизован внутри, хотя чаще всего использовался в одном потоке. Поэтому пришлось создавать его несихронизованный аналог StringBuilder, которым мы почти всегда и пользуемся.


Item 68. Предпочитайте executors и tasks вместо «сырых» потоков.

В Java 1.5 появился пакет java.util.concurrent, содержащий Executor Framework. Данный фреймворк предоставляет классы, упрощающие выполнение очередей задач в однопоточном, многопоточном режимах или по таймеру. Благодаря им больше не нужно писать свои собственные безопасные очереди и диспетчеры потоков.

Кроме того, фреймворк вводит понятие задач (tasks), представленных двумя интерфейсами Runnable и Callable. Эти интерфейсы позволяют абстрагироваться от потоков и не работать с ними напрямую.


Item 69. Предпочитайте готовые многопоточные утилиты вместо wait и notify.

Пакет java.util.concurrent кроме Executor Framework содержит ещё и потокобезопасные коллекции и синхронизаторы (synchronizers). Данный параграф рассказывает о двух последних.

Потокобезопасные коллекции — высокоэффективные имплементации стандартных интерфейсов List, Queue, Map, Set. Они синхронизованны внутри. Некоторые из них имеют особые методы типа map.putIfAbsent(key, value) для обеспечения атомарности подобных операций. Эти коллекции рекомендуется использовать вместо устаревших Collections.synchronizedXXX, поскольку первые более производительны.

Синхронизаторы — объекты, заставляющие потоки ждать друг друга, прежде, чем начать выполнение. Наиболее часто используемые: CountDownLatch, Semaphore. Наименее используемые: CyclicBarrier, Exchanger. Про них по-хорошему нужно читать в специализированных статьях и книгах.

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


Item 70. Документируйте потокобезопасность.

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

  • Неизменяемые (immutable) классы — синхронизация не нужна. Примеры — String, Long, BigInteger.
  • Безусловно потокобезопасные классы — экземпляры класса изменяемы, но синхронизованны внутри, и внешняя синхронизация не требуется. Примеры — Random, ConcurrentHashMap.
  • Условно потокобезопасные классы — некоторые методы требуют внешней синхронизации. Например, врапперы Collections.synchronizedXXX - их итераторы требуют дополнительной синхронизации.
  • Не потокобезопасные классы — их экземпляры изменяемы, нужна внешняя синхронизация. Пример — коллекции из java.util.
  • Враждебные многопоточности (thread-hostile) классы, методы — им не помогает даже внешная синхронизация. В основном, это происходит, когда экземпляры работают со статическими данными класса. Такие классы — редкость, они появляются, если кто-то не расчитывает на многопоточность при проектировании.


Item 71. Используйте ленивую инициализацию с умом.

Параграф описывает способы осуществления ленивой инициализации. Если не позаботиться о многопоточности, может случаться повторная инициализация.

Когда нужно лениво инциализировать статическое поле, используйте holder class idiom; когда не статическое поле — double-check idiom. И помните, что оптимизировать нужно только тогда, когда это действительно необходимо.


Item 72. Не полагайтесь на планировщик потоков.

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


Item 73. Избегайте использования групп потоков (thread groups).

Группы потоков были придуманы с целью изолировать java-апплеты для безопасности, но они так и не справились с этим. Сейчас этот API совершенно устарел. Нет причин использовать его в коде.


Комментариев нет:

Отправить комментарий