Рубрики
Без рубрики

Длинный аккумулятор и длинный аккумулятор на Java

Узнайте, как длинный аккумулятор и длинный аккумулятор могут помочь вам в ваших многопоточных приложениях

Автор оригинала: baeldung.

1. Обзор

В этой статье мы рассмотрим две конструкции из пакета java.util.concurrent : LongAdder и LongAccumulator .

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

2. ЛонгЭддер

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

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

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

И поэтому, когда больше потоков вызывают increment() , массив будет длиннее. Каждая запись в массиве может быть обновлена отдельно, что уменьшает конкуренцию. В связи с этим, LongAdder является очень эффективным способом увеличения счетчика из нескольких потоков.

Давайте создадим экземпляр класса LongAdder и обновим его из нескольких потоков:

LongAdder counter = new LongAdder();
ExecutorService executorService = Executors.newFixedThreadPool(8);

int numberOfThreads = 4;
int numberOfIncrements = 100;

Runnable incrementAction = () -> IntStream
  .range(0, numberOfIncrements)
  .forEach(i -> counter.increment());

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(incrementAction);
}

Результат счетчика в LongAdder недоступен до тех пор, пока мы не вызовем метод sum () . Этот метод будет перебирать все значения нижнего массива и суммировать эти значения, возвращая правильное значение. Однако мы должны быть осторожны, потому что вызов метода sum() может быть очень дорогостоящим:

assertEquals(counter.sum(), numberOfIncrements * numberOfThreads);

Иногда после вызова sum () мы хотим очистить все состояния, связанные с экземпляром LongAdder , и начать отсчет с самого начала. Для этого мы можем использовать метод sumThenReset() :

assertEquals(counter.sumThenReset(), numberOfIncrements * numberOfThreads);
assertEquals(counter.sum(), 0);

Обратите внимание, что последующий вызов метода sum() возвращает ноль, что означает, что состояние было успешно сброшено.

Кроме того, Java также предоставляет DoubleAdder для поддержания суммирования значений double с помощью API, аналогичного LongAdder.

3. Длинный Аккумулятор

LongAccumulator также является очень интересным классом, который позволяет нам реализовать алгоритм без блокировки в ряде сценариев. Например, его можно использовать для накопления результатов в соответствии с предоставленным LongBinaryOperator – это работает аналогично операции reduce() из Stream API.

Экземпляр Long Accumulator можно создать, предоставив LongBinaryOperator и начальное значение его конструктору. Важно помнить, что LongAccumulator будет работать правильно, если мы снабдим его коммутативной функцией, где порядок накопления не имеет значения.

LongAccumulator accumulator = new LongAccumulator(Long::sum, 0L);

Мы создаем Длинный аккумулятор whi ch добавит новое значение к значению, которое уже было в аккумуляторе. Мы устанавливаем начальное значение Long Accumulator равным нулю, поэтому при первом вызове метода accumulate() значение previousValue будет иметь нулевое значение.

Давайте вызовем метод accumulate() из нескольких потоков:

int numberOfThreads = 4;
int numberOfIncrements = 100;

Runnable accumulateAction = () -> IntStream
  .rangeClosed(0, numberOfIncrements)
  .forEach(accumulator::accumulate);

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(accumulateAction);
}

Обратите внимание, как мы передаем число в качестве аргумента методу accumulate () . Этот метод вызовет нашу функцию sum () .

LongAccumulator использует реализацию сравнения и подкачки, что приводит к этой интересной семантике.

Во-первых, он выполняет действие, определенное как LongBinaryOperator, , а затем проверяет, изменилось ли предыдущее значение . Если оно было изменено, действие выполняется снова с новым значением. Если нет, то ему удастся изменить значение, хранящееся в аккумуляторе.

Теперь мы можем утверждать, что сумма всех значений из всех итераций была 20200 :

assertEquals(accumulator.get(), 20200);

Интересно, что Java также предоставляет Double Accumulator с той же целью и API, но для double значений.

4. Динамическое Чередование

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

Вот простое описание того, что делает Striped64 :

Динамическое Чередование

Разные потоки обновляют разные ячейки памяти. Поскольку мы используем массив (то есть полосы) состояний, эта идея называется динамическим чередованием. Интересно, что Striped64 назван в честь этой идеи и того факта, что он работает с 64-битными типами данных.

Мы ожидаем, что динамическое чередование улучшит общую производительность. Однако то, как JVM распределяет эти состояния, может иметь контрпродуктивный эффект.

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

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

Ложное Совместное использование

Аннотация @Contended отвечает за добавление этого дополнения. Заполнение повышает производительность за счет большего потребления памяти.

5. Заключение

В этом кратком уроке мы рассмотрели LongAdder и Long Accumulator и показали, как использовать обе конструкции для реализации очень эффективных и свободных от блокировок решений.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub – это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.