1. Información general
En este artículo, veremos la construcción ThreadLocal del paquete java.lang . Esto nos da la capacidad de almacenar datos individualmente para el hilo actual y simplemente envolverlos dentro de un tipo especial de objeto.
2. API ThreadLocal
La construcción TheadLocal nos permite almacenar datos que serán accesibles solo por un hilo específico .
Digamos que queremos tener un valor entero que se incluirá con el hilo específico:
ThreadLocal threadLocalValue = new ThreadLocal();
A continuación, cuando queremos usar este valor de un hilo, solo necesitamos llamar a un método get () o set () . En pocas palabras, podemos pensar que ThreadLocal almacena datos dentro de un mapa, con el hilo como clave.
Debido a ese hecho, cuando llamamos a un método get () en el threadLocalValue , obtendremos un valor Integer para el hilo solicitante:
threadLocalValue.set(1); Integer result = threadLocalValue.get();
Podemos construir una instancia de ThreadLocal usando el método estático withInitial () y pasándole un proveedor:
ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);
Para eliminar el valor de ThreadLocal , podemos llamar al método remove () :
threadLocal.remove();
Para ver cómo usar ThreadLocal correctamente, en primer lugar, veremos un ejemplo que no usa ThreadLocal , luego reescribiremos nuestro ejemplo para aprovechar esa construcción.
3. Almacenamiento de datos de usuario en un mapa
Consideremos un programa que necesita almacenar los datos de contexto específicos del usuario por identificación de usuario dada:
public class Context { private String userName; public Context(String userName) { this.userName = userName; } }
Queremos tener un hilo por identificación de usuario. Crearemos una clase SharedMapWithUserContext que implementa la interfaz Runnable . La implementación en el método run () llama a alguna base de datos a través de la clase UserRepository que devuelve un objeto Context para un userId dado .
A continuación, almacenamos ese contexto en el ConcurentHashMap con clave de userId :
public class SharedMapWithUserContext implements Runnable { public static Map userContextPerUserId = new ConcurrentHashMap(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor }
Podemos probar fácilmente nuestro código creando e iniciando dos subprocesos para dos userIds diferentes y afirmando que tenemos dos entradas en el mapa userContextPerUserId :
SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
4. Almacenamiento de datos de usuario en ThreadLocal
Podemos reescribir nuestro ejemplo para almacenar la instancia de contexto de usuario usando un ThreadLocal . Cada hilo tendrá su propia instancia ThreadLocal .
Al usar ThreadLocal , debemos tener mucho cuidado porque cada instancia de ThreadLocal está asociada con un hilo en particular. En nuestro ejemplo, tenemos un hilo dedicado para cada userId en particular , y este hilo es creado por nosotros, por lo que tenemos control total sobre él.
El método run () obtendrá el contexto del usuario y lo almacenará en la variable ThreadLocal usando el método set () :
public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal userContext = new ThreadLocal(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor }
Podemos probarlo iniciando dos subprocesos que ejecutarán la acción para un ID de usuario determinado :
ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();
Después de ejecutar este código, veremos en la salida estándar que ThreadLocal se estableció por hilo dado:
thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}
Podemos ver que cada uno de los usuarios tiene su propio contexto .
5. ThreadLocal sy Thread Pools
ThreadLocal proporciona una API fácil de usar para limitar algunos valores a cada hilo. Esta es una forma razonable de lograr la seguridad de subprocesos en Java. Sin embargo, debemos tener mucho cuidado cuando usamos ThreadLocal sy grupos de subprocesos juntos.
Para comprender mejor la posible advertencia, consideremos el siguiente escenario:
- Primero, la aplicación toma prestado un hilo del grupo.
- Luego almacena algunos valores confinados a subprocesos en ThreadLocal del subproceso actual .
- Una vez que finaliza la ejecución actual, la aplicación devuelve el hilo prestado al grupo.
- Después de un tiempo, la aplicación toma prestado el mismo hilo para procesar otra solicitud.
- Dado que la aplicación no realizó las limpiezas necesarias la última vez, puede reutilizar los mismos datos de ThreadLocal para la nueva solicitud.
Esto puede tener consecuencias sorprendentes en aplicaciones muy concurrentes.
Una forma de resolver este problema es eliminar manualmente cada ThreadLocal una vez que hayamos terminado de usarlo. Debido a que este enfoque necesita revisiones de código rigurosas, puede ser propenso a errores.
5.1. Ampliación del ThreadPoolExecutor
Resulta que es posible extender la clase ThreadPoolExecutor y proporcionar una implementación de gancho personalizada para los métodos beforeExecute () y afterExecute () . El grupo de subprocesos llamará al método beforeExecute () antes de ejecutar cualquier cosa utilizando el subproceso prestado. Por otro lado, llamará al método afterExecute () después de ejecutar nuestra lógica.
Por lo tanto, podemos extender la clase ThreadPoolExecutor y eliminar los datos ThreadLocal en el método afterExecute () :
public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }
Si enviamos nuestras solicitudes a esta implementación de ExecutorService , entonces podemos estar seguros de que el uso de ThreadLocal y grupos de subprocesos no presentará riesgos de seguridad para nuestra aplicación.
6. Conclusión
En este artículo rápido, analizamos la construcción ThreadLocal . Implementamos la lógica que usa ConcurrentHashMap que se compartió entre subprocesos para almacenar el contexto asociado con un userId en particular . A continuación, reescribimos nuestro ejemplo para aprovechar ThreadLocal para almacenar datos que están asociados con un userId en particular y con un hilo en particular.
La implementación de todos estos ejemplos y fragmentos de código se puede encontrar en GitHub.