Guía de ThreadLocalRandom en Java

1. Información general

Generar valores aleatorios es una tarea muy común. Por eso Java proporciona la clase java.util.Random .

Sin embargo, esta clase no funciona bien en un entorno de subprocesos múltiples.

De forma simplificada, la razón del bajo rendimiento de Random en un entorno de subprocesos múltiples se debe a la contención, dado que varios subprocesos comparten la misma instancia de Random .

Para abordar esa limitación, Java introdujo la clase java.util.concurrent.ThreadLocalRandom en JDK 7, para generar números aleatorios en un entorno de subprocesos múltiples .

Veamos cómo funciona ThreadLocalRandom y cómo usarlo en aplicaciones del mundo real.

2. ThreadLocalRandom Over Random

ThreadLocalRandom es una combinación de las clases ThreadLocal y Random (más sobre esto más adelante) y está aislado del hilo actual. Por lo tanto, logra un mejor rendimiento en un entorno multiproceso simplemente evitando cualquier acceso concurrente a instancias de Random .

El número aleatorio obtenido por un hilo no se ve afectado por el otro hilo, mientras que java.util.Random proporciona números aleatorios globalmente.

Además, a diferencia de Random, ThreadLocalRandom no admite establecer la semilla de forma explícita. En cambio, anula el método setSeed (semilla larga) heredado de Random para lanzar siempre una UnsupportedOperationException si se llama.

2.1. Contención de hilos

Hasta ahora, hemos establecido que la clase Random tiene un desempeño deficiente en entornos altamente concurrentes. Para entender mejor esto, veamos cómo se implementa una de sus operaciones principales, 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)); }

Esta es una implementación de Java para el algoritmo Linear Congruential Generator. Es obvio que todos los subprocesos comparten la misma variable de instancia inicial .

Para generar el siguiente conjunto aleatorio de bits, primero intenta cambiar el valor de semilla compartido de forma atómica a través de compareAndSet o CAS para abreviar.

Cuando varios subprocesos intentan actualizar la semilla simultáneamente usando CAS, un subproceso gana y actualiza la semilla, y el resto pierde. Los hilos perdidos intentarán el mismo proceso una y otra vez hasta que tengan la oportunidad de actualizar el valor y, finalmente, generar el número aleatorio.

Este algoritmo no tiene bloqueos y diferentes subprocesos pueden progresar al mismo tiempo. Sin embargo, cuando la contención es alta, el número de fallos y reintentos de CAS perjudicará significativamente el rendimiento general.

Por otro lado, ThreadLocalRandom elimina completamente esta contención, ya que cada hilo tiene su propia instancia de Random y, en consecuencia, su propia semilla confinada .

Echemos ahora un vistazo a algunas de las formas de generar valores int, long y double aleatorios .

3. Generación de valores aleatorios mediante ThreadLocalRandom

Según la documentación de Oracle, solo necesitamos llamar al método ThreadLocalRandom.current () , y devolverá la instancia de ThreadLocalRandom para el hilo actual . Luego podemos generar valores aleatorios invocando los métodos de instancia disponibles de la clase.

Generemos un valor int aleatorio sin límites:

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

A continuación, veamos cómo podemos generar un valor int acotado aleatorio , es decir, un valor entre un límite inferior y superior dado.

Aquí hay un ejemplo de cómo generar un valor int aleatorio entre 0 y 100:

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

Tenga en cuenta que 0 es el límite inferior inclusivo y 100 es el límite superior exclusivo.

Podemos generar valores aleatorios para long y double invocando los métodos nextLong () y nextDouble () de una manera similar a como se muestra en los ejemplos anteriores.

Java 8 también agrega el método nextGaussian () para generar el siguiente valor distribuido normalmente con una media de 0.0 y una desviación estándar de 1.0 de la secuencia del generador.

Al igual que con la clase Random , también podemos usar los métodos doubles (), ints () y longs () para generar flujos de valores aleatorios.

4. Comparación de ThreadLocalRandom y Random con JMH

Veamos cómo podemos generar valores aleatorios en un entorno de subprocesos múltiples, usando las dos clases, luego comparemos su desempeño usando JMH.

Primero, creemos un ejemplo en el que todos los subprocesos comparten una sola instancia de Random. Aquí, estamos enviando la tarea de generar un valor aleatorio usando la instancia 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);
    

Comprobemos el rendimiento del código anterior utilizando la evaluación comparativa de JMH:

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

De manera similar, ahora usemos ThreadLocalRandom en lugar de la instancia Random , que usa una instancia de ThreadLocalRandom para cada hilo del grupo:

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

Aquí está el resultado de usar ThreadLocalRandom:

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

Finalmente, al comparar los resultados de JMH anteriores tanto para Random como para ThreadLocalRandom , podemos ver claramente que el tiempo promedio necesario para generar 1000 valores aleatorios usando Random es 772 microsegundos, mientras que usando ThreadLocalRandom es de alrededor de 625 microsegundos.

Por lo tanto, podemos concluir que ThreadLocalRandom es más eficiente en un entorno altamente concurrente .

Para obtener más información sobre JMH , consulte nuestro artículo anterior aquí.

5. Detalles de implementación

Es un buen modelo mental pensar en ThreadLocalRandom como una combinación de clases ThreadLocal y Random . De hecho, este modelo mental estaba alineado con la implementación real antes de Java 8.

Sin embargo, a partir de Java 8, esta alineación se rompió por completo cuando ThreadLocalRandom se convirtió en un singleton . Así es como se ve el método current () en Java 8+:

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

Es cierto que compartir una instancia aleatoria global conduce a un rendimiento subóptimo en alta contención. Sin embargo, usar una instancia dedicada por hilo también es excesivo.

Instead of a dedicated instance of Random per thread, each thread only needs to maintain its own seed value. As of Java 8, the Thread class itself has been retrofitted to maintain the seed value:

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; }

The threadLocalRandomSeed variable is responsible for maintaining the current seed value for ThreadLocalRandom. Moreover, the secondary seed, threadLocalRandomSecondarySeed, is usually used internally by the likes of ForkJoinPool.

This implementation incorporates a few optimizations to make ThreadLocalRandom even more performant:

  • 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 es una simple adición al JDK, pero puede crear un impacto notable en aplicaciones altamente concurrentes.

Y, como siempre, la implementación de todos estos ejemplos se puede encontrar en GitHub.