Многопоточность. java.util.concurrent. Atomic-типы
Дорогу осилит идущийМы завершили знакомство с новым синтаксисом, который актуален при разработке многопоточных приложений, а также с основными инструментами, которые предоставляет для работы с многопоточностью java.lang – класс Thread и методы wait(), notify(), notifyAll() у класса Object.
Но в рамках JDK есть еще масса иных инструментов, без которых разработка многопоточных приложение на Java в привычном среднему Java-разработчику виде была бы невозможна. И основная их часть находится в пакете java.util.concurrent.
Данный пакет инкапсулирует в себя массу разнообразных инструментов, заточенных под разные задачи. Часть из них – удобные обертки над уже изученными нами инструментами (например, над volatile-переменными и синхронизацией потоков), часть же – новые для нас механизмы, которые не имеют прямых аналогов за пределами многопоточной среды.
Для понимания, обозначим наиболее известные инструменты, предлагаемые пакетом java.util.concurrent:
1. Atomic-типы. То, что мы рассмотрим в рамках текущего урока;
2. Потокобезопасные коллекции. Об этой части java.util.concurrent упоминалось уже неоднократно;
3. Блокирующие и неблокирующие очереди. Формально, это тоже коллекции, но их роль несколько отличается;
4. Локи (от англ. lock - блокировка);
5. Объекты синхронизации. Наверно, можно назвать этот раздел результатом эволюции synchronized;
6. Executor’ы и ThreadPool’ы. Все то, что избавляет от необходимости работать каждый раз с классом Thread напрямую, скрывая огромное количество однотипного кода и оптимизируя использование Thread’ов в привычном нам понимании.
Как вы уже поняли, именно пакету java.util.concurrent будут посвящено большинство уроков в рамках раздела «Многопоточность». К слову, если освоите их, сможете очень сильно удивить интервьюеров на собеседовании:)
Atomic-типы
Знакомство с java.util.concurrent начнем с, пожалуй, наиболее очевидных его инструментов.
Изучая синхронизацию и актуальность ключевого слова volatile, полагаю, каждый из вас хотя бы раз подумал о том, как же тяжело постоянно отслеживать корректность взаимодействия с переменными.
Забудешь указать volatile – в потоке может не отображаться актуальное значение переменной, может вообще использоваться переменная, которую еще не инициализировали.
Неправильно реализуешь (или не реализуешь) синхронизацию при совместном использовании переменной несколькими потоками – не будет прозрачности в значении переменной в момент времени. Если данный аспект не совсем понятен – рекомендую загуглить «Гонка потоков».
Все это забирает много внимания и требует писать много однотипного кода, постоянно держа такие мелочи в фокусе. Чтобы избавить разработчиков от этого, были разработаны классы, которые гарантируют, что операции над их объектами атомарны. Т.е. во время выполнения операции одним потоком, никакой другой поток не сможет произвести свою операцию над той же переменной.
Классы атомик-типов находятся в пакете java.util.concurrent.atomic. Вот наиболее популярные из них:
· AtomicBoolean;
· AtomicInteger;
· AtomicLong.
Подозреваю, что более 90% Java-разработчиков знают лишь об этих атомик-типах. Очевидно, что они являются обертками над соответствующими примитивами: boolean, int и long. Чуть ниже мы рассмотрим основные операции, которые для них доступны.
Но кроме обозначенных выше, существуют и другие атомик-типы.
AtomicReference
Атомик-тип для ссылочных типов. Кроме очевидных операций get() и set() имеет ряд вариаций на тему «атомарно установить значение по такому принципу, если старое…» и прочих методов, которые так или иначе решают проблему атомарности чтения и записи. Методы AtomicReference преимущественно схожи с методами AtomicBoolean и AtomicInteger, подробный разбор которых будет ниже. Пока же разберем проблематику атомарности чтения-записи.
В большинстве случаев нам не просто нужно заменить старое значение на новое. Требуется обновить значение с учетом старого. А для этого его сначала нужно прочесть. Если делать это вне атомик-типов, всегда остается риск, что значение изменилось после того, как поток прочел его, но до того, как записал новое значение, на основании текущего. Это актуально как для AtomicReference, так и для других классов Atomic%Название типа%.
Проблему можно проиллюстрировать следующим образом. Пусть у нас есть переменная типа Integer. Нам нужно увеличить ее значение на 2, но с нашей переменной могут одновременно работать несколько потоков, изменяя ее в рамках своих задач.
Рассмотрим следующую ситуацию:
1. Поток 1 читает значение переменной. Получает 3 (например);
2. Поток 2 читает значение переменной. Получает 3;
3. Поток 1 записывает в переменную значение «считанное» + 2. Получается 5;
4. Поток 2 в рамках своей задачи также увеличивает переменную на 2: получается 3 (значение было считано на шаге 2) + 2 == 5.
В итоге, операция сложения была произведена дважды. Вполне логично было бы ожидать 3+2+2 == 7. Но из-за неатомарной связки операций чтение-запись – одна из операций де-факто потерялась и итоговым значением переменной стало 5.
Конечно, можно каждую такую связку операций оборачивать в synchronized. Но тогда код многопоточных приложений очень быстро станет нечитаемым из-за обилия блоков синхронизации при каждом изменении переменных. А из-за возможных взаимных блокировок (гуглить «deadlock») – еще и нерабочим.
Атомик-тип, в свою очередь предложит для подобных ситуаций метод вроде getAndAccumulate(). С его использованием схема выше превратится в нечто, что могло бы получиться при использовании synchronized:
1. Поток 1 читает значение переменной. Получает 3;
2. Поток 2 читает значение переменной. У него не получается, поток блокируется;
3. Поток 1 записывает в переменную значение 3 + 2 == 5;
4. Поток 2 наконец получает доступ к переменной и считывает ее. Получает 5;
5. Поток 2 записывает в переменную 5 + 2 == 7.
На самом деле, механизмы, используемые внутри атомик-типов немного сложнее, чем синхронизация на уровне Java, хоть и имеет схожий результат.
Вернемся к тому, какие еще атомик-типы есть в Java.
AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
Классы, предлагающие атомарные операции над массивами.
На данном этапе предлагаю просто отметить, что они существуют.
DoubleAccumulator, DoubleAdder
Классы, позволяющие совершать атомарные операции над Double.
DoubleAdder – по сути, простейший счетчик суммы, предоставляющий следующие операции:
· «прибавить» (add(double x));
· «получить текущее значение» (sum());
· «сбросить значение» (reset());
· «получить текущее значение, а затем сбросить» (sumThenReset()).
При этом данный класс весьма производителен. Если вам нужно просто считать сумму вещественных чисел в разных потоках одновременно – это хороший выбор.
DoubleAccumulator предоставляет более гибкую настройку – его конструктор принимает параметром лямбда-выражение, позволяющее описать, что требуется делать с «добавляемым» значением. Так, можно описать не только сложение, но и другие операции. От примитивных – вычитание, умножение, деление, возведение в степень и т.д. до более сложных выражений, например, с использований каких-либо коэффициентов или других особенностей расчетов, актуальных для конкретной задачи.
В дальнейшем же с помощью метода accumulate(double x) можно обновлять значение объекта, в соответствии с логикой расчета, описанной в функции-аккумуляторе из конструктора.
Также имеются схожие с DoubleAdder методы:
· «получить текущее значение» (get());
· «сбросить значение» (reset());
· «получить текущее значение, а затем сбросить» (getThenReset()).
DoubleAccumulator в среднем менее производительный, чем DoubleAdder, но за счет функции-аккумулятора может иметь намного более гибкую логику.
LongAccumulator, LongAdder
По сути, все то же самое, что и описано для аналогичных Double-типов. Только для long.
Кроме этого есть еще несколько атомик-типов для работы с ссылочными типами – AtomicMarkableReference и AtomicStampedReference. Но я не вижу особого смысла в знакомстве с ними на данном этапе – они еще более узконаправлены, чем классы, рассмотренные выше.
Возвращаясь к популярным Atomic-типам
Теперь, после поверхностного знакомства с остальными атомик-типами, пришло время вернуться к основным представителям пакета. И разобрать основные операции, которые для них доступны.
Общие для AtomicBoolean, AtomicInteger и AtomicLong операции
Данные классы не имеют общего предка, но ряд операций у них имеет одинаковый нейминг и схожий набор параметров (с поправкой на типизацию).
· get(). Возвращает текущее значение в виде примитива;
· set(newValue). Устанавливает значение, переданное параметром, вместо текущего;
· compareAndSet(expectedValue, newValue). Возвращает true, если текущее значение совпадает с expectedValue, иначе – fasle. Также если expectedValue равно актуальному значению атомика, заменяет значение на newValue;
· lazySet(newValue). Устанавливает значение в рамках текущего потока. Для остальных потоков установка значения произойдет с задержкой. Если вызвать для одного объекта в двух разных потоках одновременно – одно из значений будет потеряно. По сути, данная операция напоминает присваивание для обычной не-атомик переменной и имеет схожие проблемы в контексте многопоточной среды;
· getAndSet(newValue). Возвращает текущее значение, а затем устанавливает новое;
· compareAndExchange(expectedValue, newValue). Похож на compareAndSet(), но возвращает не boolean (результат сравнения текущего значения с expectedValue), а текущее значение атомика.
Кроме разобранных методох существуют и другие, но они менее популярны и нужны для более тонких манипуляций, с учетом допустимых эффектов видимости изменений и других нюансов, которые позволяют использовать атомик-типы более гибко, но требуют более глубоких знаний многопоточности.
Общие для AtomicInteger и AtomicLong операции
Опять же, будут разобраны основные из них.
· getAndAccumulate(x, accumulatorFunction). Возвращает текущее значение переменной. Устанавливает новым значением результат лямбда-выражения accumulatorFunction. Напоминает LongAccumulator#accumulate(), только функцию-аккумулятор можно при каждом вызове указывать разную;
· accumulateAndGet(x, accumulatorFunction). Метод аналогичен предыдущему, но возвращает уже обновленное значение;
· getAndAdd(delta). Возвращает значение атомика, а потом устанавливает новое как сумму старого и delta;
· addAndGet(delta). Метод аналогичен предыдущему, но возвращает уже обновленное значение;
· getAndDecrement() и getAndIncrement(). Возвращает значение атомика, а потом уменьшает/увеличивает его на 1;
· decrementAndGet() и incrementAndGet(). Методы аналогичны предыдущим, но возвращают уже обновленное значение;
· getAndUpdate(updateFunction). Возвращает значение атомика, а потом заменяет его на результат updateFunction. updateFunction – лямбда-выражение, принимающее параметром текущее значение атомика. Отличается от getAndAccumulate() тем, что изменяет текущее значение без влияния некого нового числа, исключительно на базе текущего значения;
· updateAndGet(updateFunction). Метод аналогичен предыдущему, но возвращает уже обновленное значение.
Полагаю, вы уже поняли, что методы атомик-типов не слишком сложные, они лишь покрывают основные сценарии использования соответствующих типов. По сути, задача этих классов заключается в предоставлении удобного интерфейса. Было бы странно делать его сложным.
В качестве заключения
С текущим набором знаний для вас не представит сложности использовать атомик-типы при решении задач. Но если хочется углубиться в теорию – рекомендую изучить общий принцип работы атомик-типов в Java. Гуглить «compare-and-swap java atomic».
Как минимум, рекомендую узнать, почему операции атомик-типов считаются неблокирующими.
С теорией на сегодня все!

Переходим к практике:
Задача 1:
Реализуйте сервис управления счетчиками. Счетчики могут добавляться и удаляться, в самих счетчиках могут изменяться значения – как увеличиваться, так и уменьшаться. Также возможен сброс счетчика до 0 (нуля).
Гарантируйте возможность безопасной работы с данным сервисом.
Задача 2:
Реализуйте метод, возвращающий число элементов равных N в двумерном массиве целых чисел. Массив и N должны передаваться как параметры метода.
Каждый одномерный массив должен быть обработан в своем потоке.
Если что-то непонятно или не получается – welcome в комменты к посту или в лс:)
Канал: https://t.me/ViamSupervadetVadens
Мой тг: https://t.me/ironicMotherfucker
Дорогу осилит идущий!