Guida a ThreadLocalRandom in Java

1. Panoramica

La generazione di valori casuali è un'attività molto comune. Questo è il motivo per cui Java fornisce la classe java.util.Random .

Tuttavia, questa classe non funziona bene in un ambiente multi-thread.

In modo semplificato, il motivo delle scarse prestazioni di Random in un ambiente multi-thread è dovuto alla contesa, dato che più thread condividono la stessa istanza Random .

Per risolvere questa limitazione, Java ha introdotto la classe java.util.concurrent.ThreadLocalRandom in JDK 7, per la generazione di numeri casuali in un ambiente multi-thread .

Vediamo come si comporta ThreadLocalRandom e come usarlo nelle applicazioni del mondo reale.

2. ThreadLocalRandom su Random

ThreadLocalRandom è una combinazione delle classi ThreadLocal e Random (ne parleremo più avanti) ed è isolata rispetto al thread corrente. Pertanto, ottiene prestazioni migliori in un ambiente multithread semplicemente evitando qualsiasi accesso simultaneo alle istanze di Random .

Il numero casuale ottenuto da un thread non è influenzato dall'altro thread, mentre java.util.Random fornisce numeri casuali a livello globale.

Inoltre, a differenza di Random, ThreadLocalRandom non supporta l'impostazione esplicita del seed. Invece, sostituisce il metodo setSeed (long seed) ereditato da Random per generare sempre un'UnsupportedOperationException se chiamato.

2.1. Discussione del thread

Finora, abbiamo stabilito che la classe Random si comporta male in ambienti altamente simultanei. Per capire meglio questo, vediamo come viene implementata una delle sue operazioni primarie, next (int) :

private final AtomicLong seed; protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed >>> (48 - bits)); }

Questa è un'implementazione Java per l'algoritmo Linear Congruential Generator. È ovvio che tutti i thread condividono la stessa variabile di istanza seed .

Per generare il successivo set casuale di bit, prova prima a modificare atomicamente il valore seed condiviso tramite compareAndSet o CAS in breve.

Quando più thread tentano di aggiornare il seme contemporaneamente utilizzando CAS, un thread vince e aggiorna il seme e il resto perde. La perdita di thread proverà lo stesso processo più e più volte fino a quando non avranno la possibilità di aggiornare il valore e infine generare il numero casuale.

Questo algoritmo è privo di blocchi e diversi thread possono avanzare contemporaneamente. Tuttavia, quando la contesa è alta, il numero di errori e tentativi CAS danneggerà in modo significativo le prestazioni complessive.

D'altra parte, ThreadLocalRandom rimuove completamente questa contesa, poiché ogni thread ha la propria istanza di Random e, di conseguenza, il proprio seed limitato .

Diamo ora uno sguardo ad alcuni dei modi per generare valori int, long e double casuali .

3. Generazione di valori casuali utilizzando ThreadLocalRandom

Come per la documentazione Oracle, dobbiamo solo chiamare il metodo ThreadLocalRandom.current () e restituirà l'istanza di ThreadLocalRandom per il thread corrente . Possiamo quindi generare valori casuali richiamando i metodi di istanza disponibili della classe.

Generiamo un valore int casuale senza limiti:

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

Successivamente, vediamo come possiamo generare un valore int limitato casuale , ovvero un valore compreso tra un dato limite inferiore e superiore.

Ecco un esempio di generazione di un valore int casuale compreso tra 0 e 100:

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

Tieni presente che 0 è il limite inferiore compreso e 100 è il limite superiore esclusivo.

Possiamo generare valori casuali per long e double invocando i metodi nextLong () e nextDouble () in modo simile a quello mostrato negli esempi precedenti.

Java 8 aggiunge anche il metodo nextGaussian () per generare il prossimo valore normalmente distribuito con una media di 0,0 e una deviazione standard di 1,0 dalla sequenza del generatore.

Come con la classe Random , possiamo anche usare i metodi doubles (), ints () e longs () per generare flussi di valori casuali.

4. Confronto tra ThreadLocalRandom e Random utilizzando JMH

Vediamo come possiamo generare valori casuali in un ambiente multi-thread, utilizzando le due classi, quindi confrontare le loro prestazioni utilizzando JMH.

Per prima cosa, creiamo un esempio in cui tutti i thread condividono una singola istanza di Random. Qui, stiamo inviando l'attività di generazione di un valore casuale utilizzando l' istanza Random a un ExecutorService:

ExecutorService executor = Executors.newWorkStealingPool(); List
    
      callables = new ArrayList(); Random random = new Random(); for (int i = 0; i { return random.nextInt(); }); } executor.invokeAll(callables);
    

Controlliamo le prestazioni del codice sopra usando il benchmarking JMH:

# Run complete. Total time: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20 771.613 ± 222.220 us/op

Allo stesso modo, ora usiamo ThreadLocalRandom invece dell'istanza Random , che utilizza un'istanza di ThreadLocalRandom per ogni thread nel pool:

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

Ecco il risultato dell'utilizzo di ThreadLocalRandom:

# Run complete. Total time: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20 624.911 ± 113.268 us/op

Infine, confrontando i risultati JMH sopra sia per Random che per ThreadLocalRandom , possiamo vedere chiaramente che il tempo medio impiegato per generare 1000 valori casuali utilizzando Random è di 772 microsecondi, mentre utilizzando ThreadLocalRandom è di circa 625 microsecondi.

Quindi, possiamo concludere che ThreadLocalRandom è più efficiente in un ambiente altamente concorrente .

Per saperne di più su JMH , consulta il nostro precedente articolo qui.

5. Dettagli di implementazione

È un buon modello mentale pensare a ThreadLocalRandom come una combinazione di classi ThreadLocal e Random . È un dato di fatto, questo modello mentale era allineato con l'implementazione effettiva prima di Java 8.

A partire da Java 8, tuttavia, questo allineamento si è interrotto completamente quando ThreadLocalRandom è diventato un singleton . Ecco come appare il metodo current () in Java 8+:

static final ThreadLocalRandom instance = new ThreadLocalRandom(); public static ThreadLocalRandom current() { if (U.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance; }

È vero che la condivisione di un'istanza Random globale porta a prestazioni non ottimali in caso di forte contesa. Tuttavia, anche l'utilizzo di un'istanza dedicata per thread è eccessivo.

Invece di un'istanza dedicata di Random per thread, ogni thread deve solo mantenere il proprio valore di inizializzazione . A partire da Java 8, la stessa classe Thread è stata adattata per mantenere il valore seed :

public class Thread implements Runnable { // omitted @jdk.internal.vm.annotation.Contended("tlr") long threadLocalRandomSeed; @jdk.internal.vm.annotation.Contended("tlr") int threadLocalRandomProbe; @jdk.internal.vm.annotation.Contended("tlr") int threadLocalRandomSecondarySeed; }

La variabile threadLocalRandomSeed è responsabile del mantenimento del valore seed corrente per ThreadLocalRandom. Inoltre, il seme secondario, threadLocalRandomSecondarySeed , viene solitamente utilizzato internamente da artisti del calibro di ForkJoinPool.

Questa implementazione incorpora alcune ottimizzazioni per rendere ThreadLocalRandom ancora più performante:

  • Avoiding false sharing by using the @Contented annotation, which basically adds enough padding to isolate the contended variables in their own cache lines
  • Using sun.misc.Unsafe to update these three variables instead of using the Reflection API
  • Avoiding extra hashtable lookups associated with the ThreadLocal implementation

6. Conclusion

This article illustrated the difference between java.util.Random and java.util.concurrent.ThreadLocalRandom.

We also saw the advantage of ThreadLocalRandom over Random in a multithreaded environment, as well as performance and how we can generate random values using the class.

ThreadLocalRandom è una semplice aggiunta al JDK, ma può creare un impatto notevole in applicazioni altamente concorrenti.

E, come sempre, l'implementazione di tutti questi esempi può essere trovata su GitHub.