В чем опасность при использовании потоков в Java?

минусы потоков Java
Антон Меринов

Антон Меринов

Автор статьи. Интересы, навыки: Профессиональное администрирование СУБД Oracle Database, веб-разработка, IT-World. Подробнее.

 
 
 

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


Оглавление статьи[Показать]


1. Угрозы безопасности

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

Листинг 1. Непотокобезопасный генератор последовательности

@NotThreadSafe
public class UnsafeSequence {
   private int value;

   /** Возвращает уникальное значение. */
   public int getNext() {
   return value++;
   }
}

 Неудачное выполнение UnsafeSequence.nextValue

Рис. 1. Неудачное выполнение UnsafeSequence.nextValue

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

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

В классе UnsafeSequence используется нестандартная аннотация: @NotThreadSafe. Это одна из нескольких настраиваемых аннотаций, используемых в этом блоге для документирования свойств конкурентности у классов и членов классов. (Другими используемыми таким образом аннотациями уровня классов будут @ThreadSafe и @Immutable) Если класс аннотирован @ThreadSafe, то его можно использовать в многопоточной среде: сопроводители получат уведомление о том, что он обеспечивает и сохраняет потокобезопасность, а инструменты анализа программного обеспечения идентифицируют возможные ошибки кодирования.

Класс UnsafeSequence иллюстрирует распространенную угрозу для конкурентности, именуемую состоянием гонки (race condition). При такой ситуации возврат уникального значения при вызове из разных потоков посредством nextValue зависит от того, как рабочая среда перемежает операции.

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

Класс UnsafeSequence можно исправить, сделав метод getNext синхронизированным, как показано в классе Sequence в листинге 2, тем самым предотвратив неудачное взаимодействие, показанное на рис. 1.

Листинг 2. Потокобезопасный генератор последовательности @ThreadSafe

public class Sequence {
   @GuardedBy("this") private int nextValue;
   public synchronized int getNext() {
   return nextValue++;
   }
}

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

2. Сбои жизнеспособности

Потокобезопасность необходима как многопоточным программам, так и однопоточным, но использование множества потоков создает для безопасности дополнительные сбои, например новые формы сбоев в жизнеспособности (liveness failure).

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

3. Угрозы производительности

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

К ним относятся контекстные переключения (context switches) — приостановки работы активных потоков для работы других потоков. Они сохраняют и восстанавливают контекст выполнения, но приводят к потере локальности и процессорного времени, затрачиваемого на планирование. 

Вас заинтересует / Intresting for you:

Распространенные заблуждения о...
Распространенные заблуждения о... 2491 просмотров Ирина Светлова Thu, 21 Jun 2018, 18:35:12
Использование потоков в Java
Использование потоков в Java 1460 просмотров Antoni Wed, 12 May 2021, 09:51:36
Выбор среды для разработки код...
Выбор среды для разработки код... 2539 просмотров Stas Belkov Sun, 10 Jun 2018, 14:21:35
Как выполнить / скомпилировать...
Как выполнить / скомпилировать... 6184 просмотров Stas Belkov Thu, 21 Jun 2018, 18:32:00
Войдите чтобы комментировать