LongAdder og LongAccumulator i Java

1. Oversikt

I denne artikkelen ser vi på to konstruksjoner fra java.util.concurrent pakke: LongAdder og LongAccumulator.

Begge er skapt for å være veldig effektive i flertrådede omgivelser, og begge utnytter veldig smarte taktikker å være låsefri og fremdeles være trådsikker.

2. LongAdder

La oss vurdere litt logikk som øker noen verdier veldig ofte, der vi bruker en AtomicLong kan være en flaskehals. Dette bruker en sammenligne-og-bytte-operasjon, som - under sterk strid - kan føre til mange bortkastede CPU-sykluser.

LongAdderbruker derimot et veldig smart triks for å redusere strid mellom tråder når disse øker.

Når vi vil øke en forekomst av LongAdder, vi må ringe økning () metode. Den implementeringen holder en rekke tellere som kan vokse etter behov.

Og så når flere tråder ringer økning ()vil matrisen være lengre. Hver post i matrisen kan oppdateres separat - noe som reduserer påstanden. På grunn av det faktum, LongAdder er en veldig effektiv måte å øke en teller fra flere tråder.

La oss lage en forekomst av LongAdder klasse og oppdater den fra flere tråder:

LongAdder-teller = ny 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); }

Resultatet av disken i LongAdder er ikke tilgjengelig før vi ringer sum() metode. Denne metoden vil gjentas over alle verdiene i undergruppen, og oppsummere verdiene som returnerer riktig verdi. Vi må være forsiktige fordi samtalen til sum() metoden kan være veldig kostbar:

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

Noen ganger etter at vi ringer sum(), vil vi fjerne alle tilstander som er assosiert med forekomsten av LongAdder og begynn å telle fra begynnelsen. Vi kan bruke sumThenReset () metode for å oppnå det:

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

Merk at den påfølgende samtalen til sum() metoden returnerer null, noe som betyr at staten ble tilbakestilt.

Videre gir Java også DoubleAdder å opprettholde en summering av dobbelt verdier med lignende API som LongAdder.

3. LongAccumulator

LongAccumulator er også en veldig interessant klasse - som lar oss implementere en låsfri algoritme i en rekke scenarier. For eksempel kan den brukes til å samle resultater i henhold til den medfølgende LongBinaryOperator - dette fungerer på samme måte som redusere() drift fra Stream API.

Forekomsten av LongAccumulator kan opprettes ved å levere LongBinaryOperator og den opprinnelige verdien til konstruktøren. Det viktige å huske det LongAccumulator vil fungere riktig hvis vi forsyner den med en kommutativ funksjon der rekkefølgen på akkumulering ikke betyr noe.

LongAccumulator akkumulator = ny LongAccumulator (Long :: sum, 0L);

Vi lager en LongAccumulator which vil legge til en ny verdi til verdien som allerede var i akkumulatoren. Vi setter inn startverdien for LongAccumulator til null, så i den første samtalen av akkumulere() metoden, den previousValue vil ha en nullverdi.

La oss påkalle akkumulere() metode fra flere tråder:

int numberOfThreads = 4; int numberOfIncrements = 100; Kjørbar accumulateAction = () -> IntStream .rangeClosed (0, numberOfIncrements) .forEach (akkumulator :: akkumulere); for (int i = 0; i <numberOfThreads; i ++) {executorService.execute (accumulateAction); }

Legg merke til hvordan vi sender et tall som et argument til akkumulere() metode. Denne metoden vil påkalle vår sum() funksjon.

De LongAccumulator bruker sammenligne-og-bytte-implementeringen - som fører til disse interessante semantikkene.

For det første utfører den en handling definert som en LongBinaryOperator, og så sjekker den om previousValue endret. Hvis den ble endret, utføres handlingen igjen med den nye verdien. Hvis ikke, lykkes det med å endre verdien som er lagret i akkumulatoren.

Vi kan nå hevde at summen av alle verdier fra alle iterasjoner var 20200:

assertEquals (accumulator.get (), 20200);

Interessant, Java gir også DoubleAccumulator med samme formål og API, men for dobbelt verdier.

4. Dynamisk striping

Alle adder- og akkumulatorimplementeringer i Java arver fra en interessant baseklasse kalt Stripet64. I stedet for å bruke bare en verdi for å opprettholde den nåværende tilstanden, bruker denne klassen en rekke tilstander for å distribuere striden til forskjellige minneplasser.

Her er en enkel skildring av hva Stripet64 gjør:

Ulike tråder oppdaterer forskjellige minneplasser. Siden vi bruker en matrise (det vil si striper) av stater, kalles denne ideen dynamisk striping. Interessant, Stripet64 er oppkalt etter denne ideen og det faktum at den fungerer på 64-biters datatyper.

Vi forventer at dynamisk striping forbedrer den generelle ytelsen. Imidlertid kan måten JVM tildeler disse statene ha en kontraproduktiv effekt.

For å være mer spesifikk, kan JVM tildele disse statene nær hverandre i dyngen. Dette betyr at noen få stater kan ligge i samme CPU-cache-linje. Derfor, oppdatering av en minneplassering kan føre til at hurtigbufferen går tapt til de nærliggende statene. Dette fenomenet, kjent som falsk deling, vil skade forestillingen.

For å forhindre falsk deling. de Stripet64 implementering legger til nok polstring rundt hver stat for å sikre at hver stat ligger i sin egen cache-linje:

De @Contended kommentar er ansvarlig for å legge til denne polstringen. Polstringen forbedrer ytelsen på bekostning av mer minneforbruk.

5. Konklusjon

I denne raske opplæringen så vi på LongAdder og LongAccumulator og vi har vist hvordan begge konstruksjonene brukes til å implementere svært effektive og låsfrie løsninger.

Implementeringen av alle disse eksemplene og kodebitene finnes i GitHub-prosjektet - dette er et Maven-prosjekt, så det skal være enkelt å importere og kjøre som det er.


$config[zx-auto] not found$config[zx-overlay] not found