12 ThreadLocal¶
ThreadLocal¶
Si creamos un objeto de una clase que implementa la interface Runnable
e iniciamos varios hilos que hagan uso de dicho mismo objeto, por defecto los hilos creados compartirán los mismos atributos, por lo que al cambiar cualquier atributo todos los hilos correspondientes se verán afectados.
Sin embargo algunas veces desearemos que algunos atributos NO sean compartidos entre todos los hilos que hacen uso del objeto Runnable
, sino que cada hilo disponga de su propia copia del atributo, de manera que al cambiar su valor NO se vean afectados el resto de hilos. Para ello la API de concurrencia de Java proporciona el mecanismo conocido como variables thread-local (locales al hilo), a través de la clase ThreadLocal<T>
. Al crear el objeto se suele suministrar el código del método initialValue()
para establecer el valor inicial de la variable.
El mecanismo de variables thread-local almacena un valor de la variable para cada hilo que la use, y nos proporciona una serie de métodos para trabajar con dicho valor:
get()
: Permite obtener el valor de la variable en el hilo en el que se ejecuta.set(valor)
: Permite establecer el valor de la variable en el hilo en el que se ejecuta.initialValue()
: La primera vez que se accede al valor de la variable si ésta no tiene valor se llamará a este método para asignarle el valor inicial para el hilo en el que se ejecuta.remove()
: Elimina el valor de la variable para el hilo en el que se ejecuta, de manera que si posteriormente se trata de obtener, se llamará al métodoinitialValue()
.
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
También podeos crear una instancia de ThreadLocal usando el método estático withInitial(), que recibe un supplier que es ejecutado para establecer el valor inicial:
Debemos tener en cuenta que las variables thread-local mantendrán su valor mientras el hilo esté vivo, lo que puede resultar problemático cuando reusemos hilos.
La API de concurrencia de Java The Java Concurrency también incluye la clase InheritableThreadLocal
para crear variables thread-local que puedan ser heredadas cuando un hilo crea otro hilo. Por defecto el hilo hijo tendrá como valor inicial de la variable el valor que tuviera dicha variable en el hilo padre. Sin embargo, podemos sobrescribir el método childValue()
para establecer el valor inicial de la variable en el hilo hijo. Dicho método recibirá como parámetro el valor que posee la variable en el hilo padre.
ThreadLocal se usa sobre todo en frameworks para almacenar un determinado contexto asociándolo a cada hilo. De esta manera se reduce la necesidad de pasar dicho contexto a cada método que lo necesita, ya que podrá acceder a él a través del ThreadLocal, aunque tiene el inconveniente de que acopla el código que lo usa al framework. Por tanto no debemos abusar del uso de ThreadLocal, al igual que no debemos abusar del uso de variable globales.
Proyecto ThreadLocal¶
En este proyecto crearemos un programa que crear un único objeto Tarea que es ejecutado por varios hilos. Crearemos dos versiones de la tarea, una errónea, en cuyo caso los resultados mostrados por pantalla son incorrectos, dado que los hilos modifican el atributo compartido de la Tarea, y otra versión correcta, en la que cada hilo dispone de su propia copia del atributo.
public class Main {
private static int NUM_HILOS = 10;
public static void main(String[] args) {
TareaCorrecta tarea = new TareaCorrecta();
Thread[] hilos = new Thread[NUM_HILOS];
// Se crean hilos que ejecutan la tarea insegura.
for (int i = 0; i < NUM_HILOS; i++) {
hilos[i] = new Thread(tarea);
hilos[i].start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class TareaErronea implements Runnable {
private String mNombre;
@Override
public void run() {
// Se obtiene la hora de inicio.
// PROBLEMA: SE ESTÁ CAMBIANDO EL NOMBRE EN TODOS LOS HILOS.
mNombre = Thread.currentThread().getName();
System.out.printf("Hilo: %s - Nombre inicio: %s\n",
Thread.currentThread().getId(), mNombre);
// Se simula el trabajo de la tarea.
try {
TimeUnit.SECONDS.sleep((int) Math.rint(Math.random() * 5));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Hilo: %s - Nombre fin: %s\n",
Thread.currentThread().getId(), mNombre);
}
}
public class TareaCorrecta implements Runnable {
private ThreadLocal<String> mNombre;
@Override
public void run() {
// Se obtiene el nombre del hilo. SOLO SE ACTUALIZA LA COPIA DEL HILO CORRESPONDIENTE.
mNombre = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return Thread.currentThread().getName();
}
};
System.out.printf("Hilo: %s - Nombre Inicio: %s\n",
Thread.currentThread().getId(), mNombre.get());
// Se simula el trabajo de la tarea.
try {
TimeUnit.SECONDS.sleep((int) Math.rint(Math.random() * 5));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Hilo: %s - Nombre Fin: %s\n",
Thread.currentThread().getId(), mNombre.get());
}
}