LongAdder en LongAccumulator in Java

1. Overzicht

In dit artikel gaan we kijken naar twee constructies uit de java.util.concurrent pakket: LongAdder en Lange accumulator.

Beide zijn gemaakt om zeer efficiënt te zijn in de multi-threaded omgeving en beide maken gebruik van zeer slimme tactieken lock-free en toch draad-veilig blijven.

2. LongAdder

Laten we eens kijken naar een logica die sommige waarden heel vaak verhoogt, waarbij een AtomicLong kan een knelpunt zijn. Dit maakt gebruik van een vergelijk-en-wissel-bewerking, die - onder zware omstandigheden - kan leiden tot veel verspilde CPU-cycli.

LongAdder, aan de andere kant, gebruikt een zeer slimme truc om conflicten tussen threads te verminderen, wanneer deze het verhogen.

Wanneer we een instantie van de LongAdder, we moeten de toename () methode. Die implementatie houdt een reeks tellers bij die op verzoek kunnen groeien.

En dus, als er meer threads bellen toename (), zal de array langer zijn. Elk record in de array kan afzonderlijk worden bijgewerkt, waardoor de strijdigheid wordt verminderd. Vanwege dat feit is de LongAdder is een zeer efficiënte manier om een ​​teller uit meerdere threads te verhogen.

Laten we een instantie maken van het LongAdder class en update het vanuit meerdere threads:

LongAdder-teller = nieuwe LongAdder (); ExecutorService executorService = Executors.newFixedThreadPool (8); int numberOfThreads = 4; int numberOfIncrements = 100; Runnable incrementAction = () -> IntStream .range (0, numberOfIncrements) .forEach (i -> counter.increment ()); voor (int i = 0; i <numberOfThreads; i ++) {executorService.execute (incrementAction); }

Het resultaat van de teller in de LongAdder is niet beschikbaar totdat we de som() methode. Die methode herhaalt alle waarden van de onderliggende array en somt die waarden op die de juiste waarde retourneren. We moeten echter voorzichtig zijn omdat de oproep naar de som() methode kan erg duur zijn:

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

Soms, nadat we hebben gebeld som(), willen we alle status wissen die is gekoppeld aan de instantie van de LongAdder en begin met tellen vanaf het begin. We kunnen de sumThenReset () methode om dat te bereiken:

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

Merk op dat de volgende oproep naar de som() methode retourneert nul, wat betekent dat de status met succes is gereset.

Bovendien biedt Java ook DoubleAdder om een ​​sommatie van te behouden dubbele waarden met een vergelijkbare API als LongAdder.

3. Lange accumulator

LongAccumulator is ook een zeer interessante klasse - waardoor we in een aantal scenario's een vergrendelingsvrij algoritme kunnen implementeren. Het kan bijvoorbeeld worden gebruikt om resultaten te verzamelen volgens het geleverde LongBinaryOperator - dit werkt op dezelfde manier als het verminderen() operatie vanuit Stream API.

Het exemplaar van de LongAccumulator kan worden gemaakt door de LongBinaryOperator en de initiële waarde voor zijn constructor. Het belangrijkste om dat te onthouden LongAccumulator zal correct werken als we het voorzien van een commutatieve functie waarbij de volgorde van accumulatie er niet toe doet.

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

We maken een LongAccumulator which voegt een nieuwe waarde toe aan de waarde die al in de accumulator stond. We stellen de beginwaarde van de LongAccumulator naar nul, dus in de eerste aanroep van de accumuleren() methode, de vorigeWaarde zal een nulwaarde hebben.

Laten we de accumuleren() methode van meerdere threads:

int numberOfThreads = 4; int numberOfIncrements = 100; Runnable accumulateAction = () -> IntStream .rangeClosed (0, numberOfIncrements) .forEach (accumulator :: accumulate); voor (int i = 0; i <numberOfThreads; i ++) {executorService.execute (accumulateAction); }

Merk op hoe we een getal als argument doorgeven aan de accumuleren() methode. Die methode roept ons op som() functie.

De LongAccumulator gebruikt de Compare-and-Swap-implementatie - die tot deze interessante semantiek leidt.

Ten eerste voert het een actie uit die is gedefinieerd als een LongBinary Operator, en dan controleert het of het vorigeWaarde veranderd. Als het is gewijzigd, wordt de actie opnieuw uitgevoerd met de nieuwe waarde. Als dit niet het geval is, slaagt het erin de waarde te wijzigen die in de accumulator is opgeslagen.

We kunnen nu beweren dat de som van alle waarden van alle iteraties was 20200:

assertEquals (accumulator.get (), 20200);

Interessant is dat Java ook biedt DoubleAccumulator met hetzelfde doel en dezelfde API maar voor dubbele waarden.

4. Dynamische strepen

Alle implementaties van adder en accumulator in Java worden geërfd van een interessante basisklasse genaamd Gestreept64. In plaats van slechts één waarde te gebruiken om de huidige status te behouden, gebruikt deze klasse een reeks toestanden om de stelling over verschillende geheugenlocaties te verdelen.

Hier is een eenvoudige weergave van wat Gestreept64 doet:

Verschillende threads werken verschillende geheugenlocaties bij. Omdat we een array (dat wil zeggen strepen) van staten gebruiken, wordt dit idee dynamische striping genoemd. Interessant is dat Gestreept64 is vernoemd naar dit idee en het feit dat het werkt op 64-bits gegevenstypen.

We verwachten dat dynamische striping de algehele prestaties zal verbeteren. De manier waarop de JVM deze staten toewijst, kan echter een contraproductief effect hebben.

Om specifieker te zijn, kan de JVM die staten bij elkaar in de heap toewijzen. Dit betekent dat een paar staten zich in dezelfde CPU-cacheregel kunnen bevinden. Daarom het bijwerken van een geheugenlocatie kan een cache-misser veroorzaken naar de nabijgelegen staten. Dit fenomeen, bekend als false sharing, zal de prestatie nadelig beïnvloeden.

Om vals delen te voorkomen. de Gestreept64 implementatie voegt voldoende opvulling toe rond elke staat om ervoor te zorgen dat elke staat zich in zijn eigen cacheregel bevindt:

De @Contended annotatie is verantwoordelijk voor het toevoegen van deze opvulling. De opvulling verbetert de prestaties ten koste van meer geheugengebruik.

5. Conclusie

In deze korte tutorial hebben we gekeken naar LongAdder en LongAccumulator en we hebben laten zien hoe u beide constructies kunt gebruiken om zeer efficiënte en vergrendelingsvrije oplossingen te implementeren.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in het GitHub-project - dit is een Maven-project, dus het moet gemakkelijk te importeren en uit te voeren zijn zoals het is.