LongAdder и LongAccumulator в Java

LongAdder иLongAccumulator в Java

1. обзор

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

Оба созданы, чтобы быть очень эффективными в многопоточной среде, и оба используют очень умную тактику, чтобыlock-free and still remain thread-safe.

2. LongAdderс

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

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

Когда мы хотим увеличить экземплярLongAdder,, нам нужно вызвать методincrement(). Эта реализацияkeeps an array of counters that can grow on demand.

Итак, когда больше потоков вызывают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() возвращает ноль, что означает, что состояние было успешно сброшено.

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

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

ЭкземплярLongAccumulator может быть создан путем передачиLongBinaryOperator и начального значения его конструктору. Важно помнить, чтоLongAccumulator will work correctly if we supply it with a commutative function where the order of accumulation does not matter.

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

Мы создаемLongAccumulator, которыйch добавит новое значение к значению, которое уже было в аккумуляторе. Мы устанавливаем начальное значениеLongAccumulator равным нулю, поэтому при первом вызове метода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,, а затем проверяет, изменился лиpreviousValue. Если оно было изменено, действие выполняется снова с новым значением. Если нет, он успешно меняет значение, которое хранится в аккумуляторе.

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

assertEquals(accumulator.get(), 20200);

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

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

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