LongAdder und LongAccumulator in Java

LongAdder undLongAccumulator in Java

1. Überblick

In diesem Artikel werden zwei Konstrukte aus dem Paketjava.util.concurrent betrachtet:LongAdder undLongAccumulator.

Beide sind so konzipiert, dass sie in der Multithread-Umgebung sehr effizient sind, und beide nutzen sehr clevere Taktiken, umlock-free and still remain thread-safe. zu sein

2. LongAdder

Betrachten wir eine Logik, die einige Werte sehr oft erhöht, wobei die Verwendung vonAtomicLong ein Engpass sein kann. Hierbei wird eine Compare-and-Swap-Operation verwendet, die - unter heftigen Konflikten - zu einer Vielzahl von verschwendeten CPU-Zyklen führen kann.

LongAdder verwendet dagegen einen sehr cleveren Trick, um Konflikte zwischen Threads zu reduzieren, wenn diese diese erhöhen.

Wenn wir eine Instanz vonLongAdder, inkrementieren möchten, müssen wir die Methodeincrement() aufrufen. Diese Implementierungkeeps an array of counters that can grow on demand.

Wenn also mehr Threadsincrement() aufrufen, ist das Array länger. Jeder Datensatz im Array kann separat aktualisiert werden, wodurch die Konkurrenz verringert wird. Aufgrund dieser Tatsache istLongAdder eine sehr effiziente Methode, um einen Zähler aus mehreren Threads zu inkrementieren.

Erstellen wir eine Instanz derLongAdder-Klasse und aktualisieren Sie sie aus mehreren Threads:

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);
}

Das Ergebnis des Zählers inLongAdder ist erst verfügbar, wenn wir die Methodesum() aufrufen. Diese Methode durchläuft alle Werte des darunter liegenden Arrays und summiert diese Werte, wobei der richtige Wert zurückgegeben wird. Wir müssen jedoch vorsichtig sein, da der Aufruf dersum()-Methode sehr kostspielig sein kann:

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

Manchmal möchten wir nach dem Aufruf vonsum() alle Zustände löschen, die der Instanz vonLongAdder zugeordnet sind, und von vorne beginnen zu zählen. Wir können diesumThenReset()-Methode verwenden, um dies zu erreichen:

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

Beachten Sie, dass der nachfolgende Aufruf der Methodesum() Null zurückgibt, was bedeutet, dass der Status erfolgreich zurückgesetzt wurde.

3. LongAccumulator

LongAccumulator ist auch eine sehr interessante Klasse, mit der wir in einer Reihe von Szenarien einen sperrfreien Algorithmus implementieren können. Zum Beispiel kann es verwendet werden, um Ergebnisse gemäß den angegebenenLongBinaryOperator zu akkumulieren - dies funktioniert ähnlich wie diereduce()-Operation von der Stream-API.

Die Instanz vonLongAccumulator kann erstellt werden, indemLongBinaryOperator und der Anfangswert an den Konstruktor übergeben werden. Das Wichtigste daran, dassLongAccumulator 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);

Wir erstellen einLongAccumulator, währendch dem Wert, der sich bereits im Akkumulator befand, einen neuen Wert hinzufügt. Wir setzen den Anfangswert vonLongAccumulator auf Null, sodass beim ersten Aufruf der Methodeaccumulate()previousValue einen Wert von Null haben.

Rufen Sie dieaccumulate()-Methode von mehreren Threads aus auf:

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);
}

Beachten Sie, wie wir eine Zahl als Argument an die Methodeaccumulate()übergeben. Diese Methode ruft die Funktionsum()auf.

LongAccumulator verwendet die Compare-and-Swap-Implementierung - was zu dieser interessanten Semantik führt.

Zuerst führt es eine alsLongBinaryOperator, definierte Aktion aus und prüft dann, ob sich diepreviousValue geändert haben. Wenn es geändert wurde, wird die Aktion mit dem neuen Wert erneut ausgeführt. Ist dies nicht der Fall, wird der im Akku gespeicherte Wert erfolgreich geändert.

Wir können nun behaupten, dass die Summe aller Werte aus allen Iterationen20200 war:

assertEquals(accumulator.get(), 20200);

4. Fazit

In diesem kurzen Tutorial haben wir unsLongAdder undLongAccumulator angesehen und gezeigt, wie beide Konstrukte verwendet werden, um sehr effiziente und sperrfreie Lösungen zu implementieren.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - dies ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.