LongAdder и LongAccumulator в Java

1. Общ преглед

В тази статия ще разгледаме две конструкции от пакета java.util.concurrent : LongAdder и LongAccumulator.

И двете са създадени, за да бъдат много ефективни в многонишковата среда и двете използват много умни тактики, за да бъдат без заключване и пак да останат безопасни за нишките.

2. LongAdder

Нека разгледаме някаква логика, която много често увеличава някои стойности, където използването на 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 за поддържане на сумиране на двойни стойности с подобен API на LongAdder.

3. LongAccumulator

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

Екземплярът на LongAccumulator може да бъде създаден чрез предоставяне на LongBinaryOperator и началната стойност на неговия конструктор. Важното е да запомните, че LongAccumulator ще работи правилно, ако му предоставим комутативна функция, където редът на натрупване няма значение.

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

Създаваме LongAccumulator съответст гл ще добави нова стойност към стойността, която вече е в акумулатора. Задаваме първоначалната стойност на LongAccumulator на нула, така че при първото извикване на метода accumulate () , предходната стойност ще има нулева стойност.

Нека извикаме метода 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 също предоставя DoubleAccumulator със същата цел и API, но за двойни стойности.

4. Динамично райе

Всички внедряващи добавки и акумулатори в Java наследяват от интересен базов клас, наречен Striped64. Вместо да използва само една стойност за поддържане на текущото състояние, този клас използва масив от състояния, за да разпространи спора на различни места в паметта.

Ето просто изображение на това, което Striped64 прави:

Различните нишки актуализират различни места в паметта. Тъй като използваме масив (т.е. ивици) от състояния, тази идея се нарича динамично ивичесто. Интересното е, че Striped64 е кръстен на тази идея и факта, че работи върху 64-битови типове данни.

Очакваме динамично ивичесто подобряване на цялостното представяне. Начинът, по който JVM разпределя тези състояния, може да има контрапродуктивен ефект.

За да бъдем по-конкретни, JVM може да разпредели тези състояния близо един до друг в купчината. Това означава, че няколко състояния могат да се намират в една и съща линия на кеша на процесора. Следователно актуализирането на едно място в паметта може да доведе до пропускане на кеш паметта до близките му състояния . Това явление, известно като фалшиво споделяне, ще навреди на представянето .

За предотвратяване на фалшиво споделяне. внедряването на Striped64 добавя достатъчно запълване около всяко състояние, за да се увери, че всяко състояние се намира в своя собствен кеш ред:

В @Contended анотацията е отговорен за добавяне на този пълнеж. Подложката подобрява производителността за сметка на повече консумация на памет.

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

В този бърз урок разгледахме LongAdder и LongAccumulator и показахме как да използваме и двете конструкции за внедряване на много ефективни и заключващи решения.

Изпълнението на всички тези примери и кодови фрагменти може да се намери в проекта GitHub - това е проект на Maven, така че трябва да е лесно да се импортира и да се изпълнява както е.