En esta lección, hablaremos en general sobre cómo trabajar con la clase java.lang.ThreadLocal<> y cómo usarla en un entorno multihilo.

La clase ThreadLocal se utiliza para almacenar variables. Una característica distintiva de esta clase es que mantiene una copia independiente separada de un valor para cada subproceso que lo usa.

Profundizando en el funcionamiento de la clase, podemos imaginar un Mapa que mapea hilos a valores, de los cuales el hilo actual toma el valor apropiado cuando necesita usarlo.

Constructor de clase ThreadLocal

Constructor Acción
Subproceso Local() Crea una variable vacía en Java

Métodos

Método Acción
conseguir() Devuelve el valor de la variable local del subproceso actual
colocar() Establece el valor de la variable local para el subproceso actual
eliminar() Elimina el valor de la variable local del hilo actual
ThreadLocal.withInitial() Método de fábrica adicional que establece el valor inicial

obtener () y establecer ()

Escribamos un ejemplo donde creamos dos contadores. La primera, una variable ordinaria, será para contar el número de hilos. El segundo lo envolveremos en un ThreadLocal . Y veremos cómo trabajan juntos. Primero, escribamos una clase ThreadDemo que herede Runnable y contenga nuestros datos y el importantísimo método run() . También agregaremos un método para mostrar los contadores en la pantalla:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();

    public void run() {
        counter++;

        if(threadLocalCounter.get() != null) {
            threadLocalCounter.set(threadLocalCounter.get() + 1);
        } else {
            threadLocalCounter.set(0);
        }
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

Con cada carrera de nuestra clase, aumentamos el counter llamando al método get() para obtener datos de la variable ThreadLocal . Si el nuevo hilo no tiene datos, lo estableceremos en 0. Si hay datos, los aumentaremos en uno. Y escribamos nuestro método principal :


public static void main(String[] args) {
    ThreadDemo threadDemo = new ThreadDemo();

    Thread t1 = new Thread(threadDemo);
    Thread t2 = new Thread(threadDemo);
    Thread t3 = new Thread(threadDemo);

    t1.start();
    t2.start();
    t3.start();

}

Al ejecutar nuestra clase, vemos que la variable ThreadLocal permanece igual independientemente del subproceso que accede a ella, pero el número de subprocesos crece.

Contador: 1
Contador: 2
Contador: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

Proceso terminado con código de salida 0

eliminar()

Para comprender cómo funciona el método de eliminación , cambiaremos ligeramente el código en la clase ThreadDemo :


if(threadLocalCounter.get() != null) {
      threadLocalCounter.set(threadLocalCounter.get() + 1);
  } else {
      if (counter % 2 == 0) {
          threadLocalCounter.remove();
      } else {
          threadLocalCounter.set(0);
      }
  }

En este código, si el contador de subprocesos es un número par, llamaremos al método remove() en nuestra variable ThreadLocal . Resultado:

Contador: 3
threadLocalCounter: 0
Contador: 2
threadLocalCounter: nulo
Contador: 1
threadLocalCounter: 0

Proceso finalizado con código de salida 0

Y aquí vemos fácilmente que la variable ThreadLocal en el segundo hilo es nula .

ThreadLocal.withInitial()

Este método crea una variable local de hilo.

Implementación de la clase ThreadDemo :


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 1);

    public void run() {
        counter++;
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

Y podemos ver el resultado de nuestro código:

Contador: 1
Contador: 2
Contador: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

Proceso finalizado con código de salida 0

¿Por qué debemos usar tales variables?

ThreadLocal proporciona una abstracción sobre las variables locales en relación con el hilo de ejecución java.lang.Thread .

Las variables ThreadLocal difieren de las ordinarias en que cada hilo tiene su propia instancia inicializada individualmente de la variable, a la que se accede a través de losmétodos get() y set() .

Cada subproceso, es decir, instancia de la clase Subproceso , tiene un mapa de variables ThreadLocal asociado con él. Las claves del mapa son referencias a objetos ThreadLocal y los valores son referencias a variables ThreadLocal "adquiridas" .

¿Por qué la clase Random no es adecuada para generar números aleatorios en aplicaciones de subprocesos múltiples?

Usamos la clase Random para obtener números aleatorios. Pero, ¿funciona igual de bien en un entorno de subprocesos múltiples? En realidad no. Random no es adecuado para entornos de subprocesos múltiples, porque cuando varios subprocesos acceden a una clase al mismo tiempo, el rendimiento se ve afectado.

Para abordar este problema, JDK 7 introdujo la clase java.util.concurrent.ThreadLocalRandom para generar números aleatorios en un entorno multihilo. Consta de dos clases: ThreadLocal y Random .

Los números aleatorios recibidos por un subproceso son independientes de otros subprocesos, pero java.util.Random proporciona números aleatorios globales. Además, a diferencia de Random , ThreadLocalRandom no admite la propagación explícita. En su lugar, anula el método setSeed() heredado de Random , por lo que siempre arroja una UnsupportedOperationException cuando se le llama.

Veamos los métodos de la clase ThreadLocalRandom :

Método Acción
ThreadLocalRandom actual() Devuelve el ThreadLocalRandom del subproceso actual.
int siguiente (int bits) Genera el siguiente número pseudoaleatorio.
double nextDouble(doble mínimo, doble límite) Devuelve un número pseudoaleatorio de una distribución uniforme entre mínimo (inclusivo) y límite (exclusivo).
int nextInt(int menos, int límite) Devuelve un número pseudoaleatorio de una distribución uniforme entre mínimo (inclusivo) y límite (exclusivo).
largo siguienteLargo(largo n) Devuelve un número pseudoaleatorio de una distribución uniforme entre 0 (inclusive) y el valor especificado (exclusivo).
long nextLong (largo mínimo, límite largo) Devuelve un número pseudoaleatorio de una distribución uniforme entre mínimo (inclusivo) y límite (exclusivo).
void setSeed (semilla larga) Lanza la excepción UnsupportedOperationException . Este generador no admite la siembra.

Obtener números aleatorios usando ThreadLocalRandom.current()

ThreadLocalRandom es una combinación de las clases ThreadLocal y Random . Logra un mejor rendimiento en un entorno de subprocesos múltiples simplemente evitando cualquier acceso simultáneo a instancias de la clase Random .

Implementemos un ejemplo que involucre varios subprocesos y veamos cómo funciona nuestra aplicación con la clase ThreadLocalRandom :


import java.util.concurrent.ThreadLocalRandom;

class RandomNumbers extends Thread {

    public void run() {
        try {
            int bound = 100;
            int result = ThreadLocalRandom.current().nextInt(bound);
            System.out.println("Thread " + Thread.currentThread().getId() + " generated " + result);
        }
        catch (Exception e) {
            System.out.println("Exception");
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

				for (int i = 0; i < 10; i++) {
            RandomNumbers randomNumbers = new RandomNumbers();
            randomNumbers.start();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime));
    }
}

Resultado de nuestro programa:

Tiempo empleado: 1
Hilo 17 generado 13
Hilo 18 generado 41
Hilo 16 generado 99
Hilo 19 generado 25
Hilo 23 generado 33
Hilo 24 generado 21
Hilo 15 generado 15
Hilo 21 generado 28
Hilo 22 generado 97
Hilo 20 generado 33

Y ahora cambiemos nuestra clase RandomNumbers y usemos Random en ella:


int result = new Random().nextInt(bound);
Tiempo empleado: 5
Hilo 20 generado 48
Hilo 19 generado 57
Hilo 18 generado 90
Hilo 22 generado 43
Hilo 24 generado 7
Hilo 23 generado 63
Hilo 15 generado 2
Hilo 16 generado 40
Hilo 17 generado 29
Hilo 21 generado 12

¡Tomar nota! En nuestras pruebas, a veces los resultados eran los mismos ya veces eran diferentes. Pero si usamos más hilos (por ejemplo, 100), el resultado se verá así:

Aleatorio — 19-25 ms
ThreadLocalRandom — 17-19 ms

En consecuencia, cuantos más subprocesos haya en nuestra aplicación, mayor será el impacto en el rendimiento al usar la clase Random en un entorno multiproceso.

Para resumir y reiterar las diferencias entre las clases Random y ThreadLocalRandom :

Aleatorio SubprocesoLocalAleatorio
Si diferentes subprocesos usan la misma instancia de Random , habrá conflictos y el rendimiento se verá afectado. No hay conflictos ni problemas, porque los números aleatorios generados son locales para el subproceso actual.
Utiliza una fórmula lineal congruente para cambiar el valor inicial. El generador de números aleatorios se inicializa utilizando una semilla generada internamente.
Útil en aplicaciones donde cada subproceso utiliza su propio conjunto de objetos aleatorios . Útil en aplicaciones donde varios subprocesos usan números aleatorios en paralelo en grupos de subprocesos.
Esta es una clase para padres. Esta es una clase para niños.