Sprievodca po ThreadLocalRandom v Jave

1. Prehľad

Generovanie náhodných hodnôt je veľmi častá úloha. To je dôvod, prečo Java poskytuje java.util.Random trieda.

Táto trieda však v prostredí s viacerými vláknami nepodáva dobré výsledky.

Zjednodušene je to dôvod zlého výkonu servera Náhodné v prostredí s viacerými vláknami je výsledkom sporu - vzhľadom na to, že viac vlákien má rovnaké Náhodné inštancia.

Na riešenie tohto obmedzenia Java predstavila java.util.concurrent.ThreadLocalRandom trieda v JDK 7 - na generovanie náhodných čísel v prostredí s viacerými vláknami.

Uvidíme ako ThreadLocalRandom výkony a ako ich používať v reálnych aplikáciách.

2. ThreadLocalRandom Koniec Náhodné

ThreadLocalRandom je kombináciou ThreadLocal a Náhodné triedy (viac o tom neskôr) a je izolovaný od aktuálneho vlákna. Teda dosahuje lepší výkon vo viacvláknovom prostredí jednoduchým vyhýbaním sa súčasnému prístupu k inštanciám Náhodné.

Náhodné číslo získané jedným vláknom nie je ovplyvnené druhým vláknom, zatiaľ čo java.util.Random poskytuje náhodné čísla globálne.

Tiež na rozdiel od Náhodné,ThreadLocalRandom nepodporuje explicitné nastavenie semena. Namiesto toho má prednosť pred setSeed (dlhé semeno) metóda zdedená z Náhodné vždy hodiť UnsupportedOperationException ak sa volá.

2.1. Napádanie závitu

Zatiaľ sme zistili, že Náhodné trieda podáva zle vo vysoko súbežných prostrediach. Aby sme tomu lepšie porozumeli, pozrime sa, ako je jedna z jej primárnych operácií, next (int), je implementovaná:

súkromné ​​konečné semeno AtomicLong; protected int next (int bits) {long oldseed, nextseed; AtomicLong seed = this.seed; do {oldseed = seed.get (); nextseed = (oldseed * multiplikátor + pridanie) & mask; } while (! seed.compareAndSet (oldseed, nextseed)); return (int) (nextseed >>> (48 - bitov))); }

Toto je implementácia Java pre algoritmus Linear Congruential Generator. Je zrejmé, že všetky vlákna zdieľajú to isté semienko inštančná premenná.

Aby sa vygenerovala ďalšia náhodná sada bitov, najskôr sa pokúsi zmeniť zdieľané semienko hodnota atomicky cez porovnaj a nastav alebo CAS v skratke.

Keď sa niekoľko vlákien pokúsi aktualizovať semienko súčasne s používaním CAS, jedno vlákno vyhráva a aktualizuje semienko, a zvyšok prehrá. Stratené vlákna budú skúšať stále ten istý proces, kým nedostanú príležitosť aktualizovať hodnotu a nakoniec vygenerujte náhodné číslo.

Tento algoritmus nie je uzamknutý a rôzne vlákna môžu postupovať súčasne. Avšak keď je tvrdenie vysoké, počet zlyhaní a opakovaných pokusov CAS významne poškodí celkový výkon.

Na druhej strane ThreadLocalRandom úplne odstraňuje toto tvrdenie, pretože každé vlákno má svoju vlastnú inštanciu Náhodné a následne aj svoje vlastné obmedzené semienko.

Poďme sa teraz pozrieť na niektoré zo spôsobov, ako generovať náhodné int, dlho a dvojitý hodnoty.

3. Generovanie náhodných hodnôt pomocou ThreadLocalRandom

Podľa dokumentácie Oracle, musíme len zavolať ThreadLocalRandom.current () metóda a vráti inštanciu ThreadLocalRandom pre aktuálne vlákno. Potom môžeme generovať náhodné hodnoty vyvolaním dostupných inštančných metód triedy.

Vytvorme náhodné int hodnota bez hraníc:

int unboundedRandomValue = ThreadLocalRandom.current (). nextInt ());

Ďalej sa pozrime, ako môžeme generovať náhodne ohraničené int hodnota, čo znamená hodnotu medzi danou dolnou a hornou hranicou.

Tu je príklad generovania náhodného int hodnota medzi 0 a 100:

int boundedRandomValue = ThreadLocalRandom.current (). nextInt (0, 100);

Upozorňujeme, že 0 je spodná hranica vrátane a 100 je horná hranica.

Môžeme vygenerovať náhodné hodnoty pre dlho a dvojitý dovolaním nextLong () a nextDouble () metód podobným spôsobom, ako je uvedené v príkladoch vyššie.

Java 8 tiež pridáva nextGaussian () metóda na generovanie ďalšej normálne distribuovanej hodnoty s priemerom 0,0 a smerodajnou odchýlkou ​​1,0 od sekvencie generátora.

Rovnako ako v prípade Náhodné triedy, môžeme použiť aj štvorhra (), ints () a túži () metódy generovania tokov náhodných hodnôt.

4. Porovnávanie ThreadLocalRandom a Náhodné Pomocou JMH

Pozrime sa, ako môžeme vygenerovať náhodné hodnoty v prostredí s viacerými vláknami pomocou dvoch tried a potom porovnať ich výkon pomocou JMH.

Najprv si vytvorme príklad, keď všetky vlákna zdieľajú jednu inštanciu Náhodné. Tu zadávame úlohu generovania náhodnej hodnoty pomocou Náhodné inštancia do ExecutorService:

ExecutorService Exekútor = Executors.newWorkStealingPool (); Zoznam callables = new ArrayList (); Random random = nový Random (); for (int i = 0; i {return random.nextInt ();}); } executor.invokeAll (callables);

Poďme skontrolovať výkon vyššie uvedeného kódu pomocou JMH benchmarkingu:

# Spustenie dokončené. Celkový čas: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20 771,613 ± 222,220 us / op

Podobne poďme teraz použiť ThreadLocalRandom namiesto Náhodné inštancia, ktorá používa jednu inštanciu z ThreadLocalRandom pre každé vlákno v bazéne:

ExecutorService Exekútor = Executors.newWorkStealingPool (); Zoznam callables = new ArrayList (); for (int i = 0; i {return ThreadLocalRandom.current (). nextInt ();}); } executor.invokeAll (callables);

Tu je výsledok použitia ThreadLocalRandom:

# Spustenie dokončené. Celkový čas: 00:00:36 Benchmark Mode Cnt Skóre Chyba Jednotky ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom priem. 20 624,911 ± 113,268 us / op

Nakoniec porovnaním vyššie uvedených výsledkov JMH pre obidve Náhodné a ThreadLocalRandom, môžeme jasne vidieť, že priemerný čas potrebný na vygenerovanie 1000 náhodných hodnôt pomocou Náhodné je 772 mikrosekúnd, zatiaľ čo použitie ThreadLocalRandom je to okolo 625 mikrosekúnd.

Môžeme teda dospieť k záveru, že ThreadLocalRandom je účinnejšia vo vysoko súbežnom prostredí.

Ak sa chcete dozvedieť viac o JMH, pozrite si náš predchádzajúci článok tu.

5. Podrobnosti implementácie

Je to dobrý mentálny model, na ktorý možno myslieť ThreadLocalRandom ako kombinácia ThreadLocal a Náhodné triedy. Tento mentálny model bol v skutočnosti zosúladený so skutočnou implementáciou pred verziou Java 8.

Od verzie Java 8 sa však toto zarovnanie úplne rozpadlo ako ThreadLocalRandom sa stal singletonom. Tu je postup, ako prúd () metóda vyzerá v prostredí Java 8+:

statická konečná inštancia ThreadLocalRandom = nová ThreadLocalRandom (); public static ThreadLocalRandom current () {if (U.getInt (Thread.currentThread (), PROBE) == 0) localInit (); návratová inštancia; }

Je pravda, že zdieľanie jedného globálneho Náhodné inštancia vedie k suboptimálnemu výkonu pri vysokom spore. Používanie jednej vyhradenej inštancie na vlákno je však tiež prehnané.

Namiesto vyhradenej inštancie Náhodné na jedno vlákno si musí každé vlákno udržiavať iba svoje vlastné semienko hodnotu. Od verzie Java 8 sa Závit samotná trieda bola dovybavená na údržbu semienko hodnota:

public class Thread implements Runnable {// vynechané @ jdk.internal.vm.annotation.Contended ("tlr") dlhé vláknoLocalRandomSeed; @ jdk.internal.vm.annotation.Contended ("tlr") int threadLocalRandomProbe; @ jdk.internal.vm.annotation.Contended ("tlr") int threadLocalRandomSecondarySeed; }

The threadLocalRandomSeed premenná je zodpovedná za udržiavanie aktuálnej počiatočnej hodnoty pre ThreadLocalRandom. Navyše, sekundárne semeno, threadLocalRandomSecondarySeed, sa zvyčajne interne používa ako ForkJoinPool.

Táto implementácia obsahuje niekoľko optimalizácií, ktoré je potrebné vykonať ThreadLocalRandom ešte výkonnejší:

  • Zabráňte falošnému zdieľaniu pomocou @Contented anotácia, ktorá v podstate pridáva dostatok výplne na izoláciu sporných premenných v ich vlastných riadkoch vyrovnávacej pamäte
  • Použitím slnko.misc.nebezpečný namiesto použitia Reflection API aktualizovať tieto tri premenné
  • Vyhýbanie sa ďalším hashtable vyhľadávaniam súvisiacim s ThreadLocal implementácia

6. Záver

Tento článok ilustroval rozdiel medzi java.util.Random a java.util.concurrent.ThreadLocalRandom.

Videli sme aj výhodu ThreadLocalRandom cez Náhodné vo viacvláknovom prostredí, ako aj výkon a to, ako môžeme pomocou triedy generovať náhodné hodnoty.

ThreadLocalRandom je jednoduchý doplnok k JDK, ale môže vytvoriť značný dopad vo vysoko súbežných aplikáciách.

A ako vždy, implementáciu všetkých týchto príkladov možno nájsť na stránkach GitHub.


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