LongAdder és LongAccumulator a Java-ban

1. Áttekintés

Ebben a cikkben két konstrukciót fogunk megvizsgálni a java.util.egyidejű csomag: LongAdder és LongAccumulator.

Mindkettő úgy lett létrehozva, hogy nagyon hatékony legyen a többszálas környezetben, és mindkettő nagyon okos taktikát alkalmaz zármentes és továbbra is menetbiztos marad.

2. LongAdder

Vegyünk egy logikát, amely bizonyos értékeket nagyon gyakran növel, ahol az AtomicLong szűk keresztmetszet lehet. Ez egy összehasonlító és cserélhető műveletet használ, amely - nagy vita esetén - sok pazarolt CPU-ciklust eredményezhet.

LongAddermásrészt egy nagyon okos trükköt használ a szálak közötti viták csökkentésére, amikor ezek növelik.

Amikor növelni akarjuk a LongAdder, hívnunk kell a növekedés() módszer. Az a megvalósítás tárol egy sor számlálót, amelyek igény szerint növekedhetnek.

És ha több szál hív növekedés(), a tömb hosszabb lesz. A tömb minden rekordja külön-külön frissíthető - ezzel csökkentve a versengést. Ennek a ténynek köszönhetően a LongAdder nagyon hatékony módja annak, hogy több szálból növelje a számlálót.

Hozzunk létre egy példányt a LongAdder osztály és frissítse több szálról:

LongAdder számláló = new LongAdder (); ExecutorService végrehajtóService = Executors.newFixedThreadPool (8); int számOfThreads = 4; int számOfInkrementek = 100; Futtatható incrementAction = () -> IntStream .range (0, numberOfIncrements) .forEach (i -> counter.increment ()); for (int i = 0; i <numberOfThreads; i ++) {végrehajtóSzolgáltatás.execute (incrementAction); }

A számláló eredménye a LongAdder nem érhető el, amíg nem hívjuk a összeg() módszer. Ez a módszer az alatta lévő tömb összes értékét iterálni fogja, és összegzi ezeket az értékeket, amelyek visszaadják a megfelelő értéket. Óvatosnak kell lennünk, mert a összeg() módszer nagyon költséges lehet:

assertEquals (számláló.sum (), numberOfIncrements * numberOfThreads);

Néha, miután felhívjuk összeg(), törölni akarunk minden olyan állapotot, amely a LongAdder és kezdd el a számolást az elejétől. Használhatjuk a sumThenReset () módszer ennek elérésére:

assertEquals (számláló.sumThenReset (), numberOfIncrements * numberOfThreads); assertEquals (számláló.sum (), 0);

Ne feledje, hogy a következő hívás a összeg() A metódus nulla értéket ad vissza, ami azt jelenti, hogy az állapot sikeresen visszaállt.

Sőt, a Java is nyújt DoubleAdder összegzésének fenntartása kettős értékekhez hasonló API-val LongAdder.

3. LongAccumulator

LongAccumulator szintén nagyon érdekes osztály - amely lehetővé teszi számunkra, hogy egy zármentes algoritmust számos forgatókönyvben megvalósítsunk. Például felhasználható az eredmények összegyűjtésére a mellékelt módon LongBinaryOperator - ez hasonlóan működik, mint a csökkenteni () művelet a Stream API-ból.

A LongAccumulator létrehozásával létrehozható LongBinaryOperator és a kezdőérték a konstruktora számára. Fontos megjegyezni ezt LongAccumulator akkor fog megfelelően működni, ha olyan kommutatív funkcióval látjuk el, ahol a felhalmozás sorrendje nem számít.

LongAccumulator akumulátor = új LongAccumulator (hosszú :: összeg, 0L);

Készítünk egy LongAccumulator which új értéket ad hozzá az értékhez, amely már benne volt az akkumulátorban. Beállítjuk a LongAccumulator nullára, tehát a felhalmozni () módszer, az előzőValue értéke nulla lesz.

Hívjuk meg a felhalmozni () módszer több szálból:

int számOfThreads = 4; int számOfInkrementek = 100; Futtatható akkumulátorAction = () -> IntStream .rangeClosed (0, numberOfIncrements) .forEach (akkumulátor :: felhalmozódás); for (int i = 0; i <numberOfThreads; i ++) {végrehajtóSzolgáltatás.execute (felhalmozódikAction); }

Figyelje meg, hogyan adunk át egy számot érvként a felhalmozni () módszer. Ez a módszer a miénkre hivatkozik összeg() funkció.

A LongAccumulator az összehasonlítás és cserélés megvalósítását használja - ami ezekhez az érdekes szemantikákhoz vezet.

Először a következő műveletet hajtja végre: LongBinaryOperator, majd ellenőrzi, hogy a előzőValue megváltozott. Ha megváltoztatta, a művelet újra végrehajtásra kerül az új értékkel. Ha nem, akkor sikerül megváltoztatni az akkumulátorban tárolt értéket.

Most azt állíthatjuk, hogy az összes iteráció értékének összege volt 20200:

assertEquals (accumulator.get (), 20200);

Érdekes módon a Java is nyújt DoubleAccumulator ugyanazzal a céllal és API-val, de kettős értékek.

4. Dinamikus csíkozás

A Java összes összeadó és akkumulátoros implementációja egy érdekes alaposztályból származik Csíkos64. Ahelyett, hogy csak egy értéket használna az aktuális állapot fenntartásához, ez az osztály egy tömb állapotot használ a verseny elosztására különböző memóriahelyeken.

Itt van egy egyszerű ábrázolás arról, hogy mit Csíkos64 csinál:

Különböző szálak frissítik a memória különböző helyeit. Mivel állapotok tömbjét (vagyis csíkokat) használjuk, ezt az elképzelést dinamikus csíkozásnak nevezzük. Érdekes módon, Csíkos64 elnevezése erről az ötletről és arról a tényről szól, hogy 64 bites adattípusokon működik.

Arra számítunk, hogy a dinamikus csíkozás javítja az általános teljesítményt. Azonban a JVM ezen állapotok kiosztásának módja kontraproduktív hatást gyakorolhat.

Pontosabban: a JVM kioszthatja ezeket az állapotokat egymás közelében a kupacban. Ez azt jelenti, hogy néhány állam ugyanabban a CPU gyorsítótárban tartózkodhat. Ebből kifolyólag, egy memóriahely frissítése gyorsítótár-hiányt okozhat a közeli állapotokhoz. Ez a jelenség, amelyet hamis megosztásnak neveznek, ártani fog az előadásnak.

A hamis megosztás megakadályozása érdekében. a Csíkos64 A megvalósítás elegendő betétet ad hozzá az egyes államok körül annak biztosításához, hogy minden állapot a saját gyorsítótárában legyen:

A @Contended ennek a kitöltésnek a feliratozásáért felelős. A kitöltés javítja a teljesítményt a nagyobb memóriafelhasználás rovására.

5. Következtetés

Ebben a gyors bemutatóban megnéztük LongAdder és LongAccumulator és megmutattuk, hogyan lehet mindkét konstrukciót nagyon hatékony és zármentes megoldások megvalósítására használni.

Ezeknek a példáknak és kódrészleteknek a megvalósítása megtalálható a GitHub projektben - ez egy Maven projekt, ezért könnyen importálhatónak és futtathatónak kell lennie.