LongAdder i LongAccumulator u Javi

1. Pregled

U ovom ćemo članku pogledati dva konstrukta iz java.util.concurrent paket: LongAdder i LongAcumulator.

Obje su stvorene da budu vrlo učinkovite u okruženju s više navoja i obje koriste vrlo pametnu taktiku bez zaključavanja i dalje ostaju bez niti.

2. LongAdder

Razmotrimo logiku koja vrlo često uvećava neke vrijednosti kada se koristi AtomicLong može biti usko grlo. Ovo koristi operaciju usporedbe i zamjene, koja - pod teškim raspravama - može dovesti do puno izgubljenog CPU ciklusa.

LongAdder, s druge strane, koristi vrlo pametan trik kako bi smanjio svađu između niti, kada je one povećavaju.

Kada želimo povećati primjerak datoteke LongAdder, moramo nazvati prirast () metoda. Ta provedba čuva niz brojača koji mogu rasti na zahtjev.

I tako, kad se javi više niti prirast (), niz će biti duži. Svaki zapis u polju može se zasebno ažurirati - smanjujući spor. Zbog te činjenice LongAdder je vrlo učinkovit način za povećavanje brojača iz više niti.

Stvorimo instancu LongAdder klase i ažurirajte je iz više niti:

LongAdder brojač = novi LongAdder (); ExecutorService executorService = Izvršitelji.newFixedThreadPool (8); int numberOfThreads = 4; int numberOfIncrements = 100; Izvodljivi inkrementAction = () -> IntStream .range (0, numberOfIncrements) .forEach (i -> counter.increment ()); for (int i = 0; i <numberOfThreads; i ++) {executorService.execute (incrementAction); }

Rezultat brojača u LongAdder nije dostupan dok ne nazovemo iznos() metoda. Ta će metoda ponoviti sve vrijednosti donjeg polja i zbrojiti te vrijednosti vraćajući ispravnu vrijednost. Moramo biti oprezni jer je poziv na iznos() metoda može biti vrlo skupa:

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

Ponekad, nakon što nazovemo iznos(), želimo očistiti sva stanja koja su povezana s instancom LongAdder i počnite brojati od početka. Možemo koristiti sumThenReset () metoda da se to postigne:

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

Imajte na umu da je sljedeći poziv na iznos() metoda vraća nulu što znači da je stanje uspješno resetirano.

Štoviše, Java također nudi DoubleAdder da bi se održao zbroj dvostruko vrijednosti sa sličnim API-jem LongAdder.

3. LongAcumulator

LongAcumulator je također vrlo zanimljiva klasa - koja nam omogućuje implementaciju algoritma bez zaključavanja u brojne scenarije. Na primjer, može se koristiti za prikupljanje rezultata prema isporučenom LongBinaryOperator - ovo radi slično kao smanjiti() rad iz Stream API-ja.

Primjer LongAcumulator može se stvoriti isporukom LongBinaryOperator i početna vrijednost njegovom konstruktoru. Važno je to upamtiti LongAcumulator ispravno će raditi ako mu isporučimo komutativnu funkciju gdje redoslijed nakupljanja nije važan.

LongAccumulator akumulator = novi LongAccumulator (Long :: sum, 0L);

Stvaramo a LongAcumulator whiCH će dodati novu vrijednost vrijednosti koja je već bila u akumulatoru. Postavljamo početnu vrijednost LongAcumulator na nulu, dakle u prvom pozivu akumulirati() metoda, previousValue imat će nultu vrijednost.

Zazovimo akumulirati() metoda iz više niti:

int numberOfThreads = 4; int numberOfIncrements = 100; Izvodljivo accumulateAction = () -> IntStream .rangeClosed (0, numberOfIncrements) .forEach (akumulator :: akumulirati); for (int i = 0; i <numberOfThreads; i ++) {executorService.execute (accumulateAction); }

Primijetite kako broj prosljeđujemo kao argument akumulirati() metoda. Ta će se metoda pozvati na našu iznos() funkcija.

The LongAcumulator koristi implementaciju usporedi i zamijeni - što dovodi do ove zanimljive semantike.

Prvo, izvršava radnju definiranu kao a LongBinaryOperator, a zatim provjerava je li previousValue promijenio. Ako je promijenjena, akcija se ponovno izvršava s novom vrijednošću. Ako nije, uspijeva promijeniti vrijednost koja je pohranjena u akumulatoru.

Sada možemo tvrditi da je zbroj svih vrijednosti iz svih ponavljanja bio 20200:

assertEquals (akumulator.get (), 20200);

Zanimljivo je da Java također nudi DoubleAccumulator s istom svrhom i API-jem, ali za dvostruko vrijednosti.

4. Dinamičko pruganje

Sve implementacije zbrajača i akumulatora u Javi nasljeđuju iz zanimljive osnovne klase tzv Prugasta64. Umjesto da koristi samo jednu vrijednost za održavanje trenutnog stanja, ova klasa koristi niz stanja za distribuciju spora na različita memorijska mjesta.

Evo jednostavnog prikaza čega Prugasta64 radi:

Različite niti ažuriraju različita memorijska mjesta. Budući da koristimo niz (tj. Pruge) stanja, ta se ideja naziva dinamičko provlačenje. Zanimljivo, Prugasta64 je dobio ime po ovoj ideji i činjenici da radi na 64-bitnim vrstama podataka.

Očekujemo da će dinamičko crtanje poboljšati ukupne performanse. Međutim, način na koji JVM dodjeljuje ta stanja može imati kontraproduktivan učinak.

Da budemo precizniji, JVM može ta stanja dodijeliti jedno drugome u hrpu. To znači da nekoliko država može boraviti u istoj liniji CPU predmemorije. Stoga, ažuriranje jedne memorijske lokacije može uzrokovati promašaj predmemorije u obližnja stanja. Ova pojava, poznata kao lažno dijeljenje, naštetit će izvedbi.

Da bi se spriječilo lažno dijeljenje. the Prugasta64 implementacija dodaje dovoljno obloga oko svakog stanja kako bi bila sigurna da se svako stanje nalazi u svojoj vlastitoj predmemoriji:

The @ Sadržaj napomena je odgovorna za dodavanje ovog popunjavanja. Podstava poboljšava performanse na štetu veće potrošnje memorije.

5. Zaključak

U ovom smo brzom vodiču pogledali LongAdder i LongAcumulator i pokazali smo kako koristiti obje konstrukcije za implementaciju vrlo učinkovitih rješenja bez zaključavanja.

Provedbu svih ovih primjera i isječaka koda možete pronaći u projektu GitHub - ovo je Maven projekt, pa bi ga trebalo lako uvesti i pokrenuti kakav jest.