ThreadLocal en Java: cuándo y cómo usarlo
Aprende qué es ThreadLocal en Java, cómo usarlo para mantener variables por hilo y cómo evitar memory leaks con remove(). Incluye ejemplos de código.

ThreadLocal en Java: cuándo y cómo usarlo
ThreadLocal en Java es una clase que guarda una copia independiente de una variable para cada hilo. Cada hilo accede a su propio valor sin sincronización, lo que evita condiciones de carrera. Se usa con SimpleDateFormat, contexto por request e IDs de transacción. En pools de hilos exige remove() en finally para evitar fugas de memoria.
Última actualización: agosto 2026
Contenido Rápido
- ThreadLocal guarda una copia de la variable por cada hilo
- Cada hilo lee y escribe su propio valor sin
synchronized - Métodos clave:
get(),set(),withInitial()yremove() - Uso típico: SimpleDateFormat, contexto por request, IDs de transacción
- En pools de hilos, olvidar
remove()provoca fugas de memoria - Alternativas: parámetros,
InheritableThreadLocaly virtual threads
Cuando varios hilos trabajan sobre un mismo dato, el primer reflejo es proteger el acceso con synchronized. ThreadLocal propone otra vía: en lugar de compartir el dato y protegerlo, cada hilo guarda su propia copia y no hay nada que proteger. Aquí verás qué es, cómo funciona por dentro y cuándo conviene usarlo.
Qué es ThreadLocal en Java
ThreadLocal es una clase de Java que asocia un valor con el hilo que lo consulta. Cuando dos hilos llaman a get() sobre el mismo objeto, cada uno recibe su propio valor, aislado del otro. No es comunicación entre hilos: es aislamiento.
La idea es simple. En vez de pasar el mismo dato por toda una cadena de métodos o de guardarlo en una variable estática compartida, cada hilo guarda su copia y la recupera cuando la necesita. Se usa para datos per-thread: el usuario de una petición HTTP, la conexión abierta para un request o el formato de fecha en uso.
Cómo funciona ThreadLocal
Internamente, cada hilo mantiene un mapa de valores de ThreadLocal. Cuando un hilo llama a set(), el valor se guarda en la entrada de ese hilo. Cuando llama a get(), se lee de esa misma entrada. Los hilos no se ven entre sí, así que no hace falta synchronized ni Lock para proteger el acceso.
Ese detalle es importante: la seguridad que aporta ThreadLocal no es global, es por hilo. Si dos hilos necesitan cooperar sobre un mismo dato, ThreadLocal no sirve; para eso están las herramientas del paquete java.util.concurrent, como el mutex o las colas bloqueantes del patrón productor-consumidor.
Métodos principales de ThreadLocal
La API de ThreadLocal es pequeña y consta de cuatro métodos:
get(): devuelve el valor del hilo actual. Si no hay valor y se definió un inicial, lo devuelve; si no, devuelvenull.set(valor): asigna el valor al hilo actual.withInitial(Supplier): crea un ThreadLocal con un valor inicial calculado bajo demanda. Es la forma recomendada de inicializar.remove(): elimina el valor del hilo actual. Es imprescindible en pools de hilos para evitar fugas de memoria.
Ejemplo básico con dos hilos
Veamos un ejemplo mínimo. Dos hilos incrementan el mismo ThreadLocal; cada uno parte de 0, así que ambos imprimen 1 sin interferirse:
public class ContadorPorHilo {
private static final ThreadLocal<Integer> contador =
ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable tarea = () -> {
contador.set(contador.get() + 1);
System.out.println(Thread.currentThread().getName()
+ ": " + contador.get());
contador.remove();
};
new Thread(tarea, "Hilo-A").start();
new Thread(tarea, "Hilo-B").start();
}
}
No hay synchronized ni Lock. El incremento no es atómico, pero como cada hilo trabaja sobre su propia copia, no existe la condición de carrera del contador compartido.
Pon en práctica este ejemplo y experimenta con ThreadLocal en el Java Playground de CodeJa, con ejecución de código en el navegador.
Cuándo usar ThreadLocal
ThreadLocal brilla en tres escenarios concretos: objetos no thread-safe, contexto por request y trazabilidad.
SimpleDateFormat no es thread-safe
El caso más citado es SimpleDateFormat. No es thread-safe: su estado interno se corrompe cuando varios hilos la usan a la vez, produce fechas incorrectas o lanza NumberFormatException bajo carga. ThreadLocal entrega una instancia a cada hilo:
import java.text.SimpleDateFormat;
import java.util.Date;
public class FormateadorPorHilo {
private static final ThreadLocal<SimpleDateFormat> formateador =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static void main(String[] args) {
Runnable tarea = () -> {
System.out.println(formateador.get().format(new Date()));
formateador.remove();
};
new Thread(tarea, "Hilo-A").start();
new Thread(tarea, "Hilo-B").start();
}
}
Cada hilo recibe su propia instancia con la misma configuración, y el formateo deja de ser un punto de fallo.
Contexto por request
En aplicaciones web, cada petición se procesa en un hilo. Guardar el usuario autenticado, el identificador de la petición o la configuración regional en un ThreadLocal permite acceder a esos datos desde cualquier punto de la cadena de llamadas sin pasarlos como parámetros.
IDs de transacción
Lo mismo aplica a la trazabilidad. Un ID de transacción o de correlación guardado en un ThreadLocal se lee desde el logging, los filtros y los interceptores para enlazar todas las trazas de una operación.
Memory leaks en pools de hilos: el gotcha crítico
ThreadLocal tiene un problema serio con los pools de hilos. Sus hilos no se destruyen al terminar una tarea: quedan vivos esperando la siguiente. El valor guardado en un ThreadLocal queda referenciado por el hilo, y si apunta a objetos grandes, no se liberan nunca, aunque la tarea que los creó ya haya terminado. Es una fuga que crece con cada tarea y puede tumbar la aplicación con un OutOfMemoryError.
El patrón correcto es limpiar siempre con remove() en un bloque finally:
public class ContextoPorRequest {
private static final ThreadLocal<String> usuarioActual =
new ThreadLocal<>();
public static void procesar(String usuario) {
try {
usuarioActual.set(usuario);
System.out.println("Procesando: " + usuarioActual.get());
} finally {
usuarioActual.remove();
}
}
}
El finally garantiza la limpieza incluso si el procesamiento lanza una excepción. Sin esa llamada, un hilo reutilizado recupera en la siguiente tarea el valor de la anterior, lo que además de la fuga produce datos incorrectos.
El ejemplo siguiente muestra por qué el coste es alto: cada tarea guarda un array de un megabyte en el ThreadLocal. Sin remove(), ese array queda retenido por el hilo del pool para siempre:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PiscinaDeHilos {
private static final ThreadLocal<byte[]> datos =
new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService piscina = Executors.newFixedThreadPool(2);
for (int i = 0; i < 1000; i++) {
piscina.execute(() -> {
try {
datos.set(new byte[1024 * 1024]);
// procesamiento con datos
} finally {
datos.remove();
}
});
}
piscina.shutdown();
}
}
Con remove() en finally, el array se libera al terminar cada tarea. Esa llamada es la diferencia entre un programa correcto y un fallo de producción intermitente.
Cuándo NO usar ThreadLocal
ThreadLocal no es la primera opción en varios escenarios. Si el dato se usa en un solo método, pásalo como parámetro: es más explícito, más fácil de testear y no deja estado oculto. Si debe propagarse a hilos hijos, usa InheritableThreadLocal, que copia el valor al crear un hilo nuevo. Y si buscas evitar el estado por hilo por completo, los virtual threads de Project Loom cambian las reglas, como veremos abajo.
Tampoco uses ThreadLocal para comunicación entre hilos ni como sustituto de sincronización. Su propósito es el aislamiento, no el intercambio de datos.
ThreadLocal vs parámetros vs InheritableThreadLocal
La elección depende de cómo quieras que fluya el dato:
| Característica | Parámetros | ThreadLocal | InheritableThreadLocal |
|---|---|---|---|
| Alcance | Un método | Un hilo | Hilo y sus hijos |
| Limpieza | Automática | remove() manual | remove() manual |
| Visibilidad | Explícita | Implícita | Implícita |
| Uso típico | Datos de entrada | Contexto por request | Contexto heredado |
| Riesgo principal | Código verboso | Memory leak | Memory leak heredado |
Virtual threads y ThreadLocal
Los virtual threads de Project Loom pueden ser millones, y cada uno con su propio ThreadLocal. El modelo sigue funcionando, con un matiz: un virtual thread comparte el hilo de plataforma que lo ejecuta, y al suspenderse en un punto de bloqueo, el framework guarda y restaura los ThreadLocal asociados. El coste por hilo es pequeño, pero con millones de threads, un dato pesado en ThreadLocal se multiplica.
La recomendación oficial es la misma que con los pools: limpiar los ThreadLocal al terminar y usarlos con moderación. El javadoc de la documentación oficial de ThreadLocal aconseja declararlos como campos estáticos privados y eliminarlos cuando el hilo ya no los necesita. Si puedes pasar el contexto como parámetro, hazlo.
Preguntas Frecuentes
¿Qué es ThreadLocal en Java?
ThreadLocal es una clase que almacena un valor independiente por cada hilo. Cada hilo que consulta get() recibe su propia copia, aislada de los demás. Es un mecanismo de aislamiento, no de comunicación entre hilos.
¿Cuándo debo usar ThreadLocal?
Úsalo para datos que pertenecen al hilo por naturaleza, como el usuario de una petición HTTP o una instancia de SimpleDateFormat. Es útil cuando pasarlo por parámetros ensucia todas las firmas. Si el dato se usa en un solo método, los parámetros son la opción más simple y segura.
¿ThreadLocal puede causar memory leaks?
Sí, especialmente en pools de hilos. Si no se llama a remove(), el valor queda referenciado por el hilo del pool, que vive para siempre, y la memoria nunca se libera. El patrón correcto es envolver el uso en un bloque try/finally y llamar a remove() en el finally.
¿Qué diferencia hay entre ThreadLocal e InheritableThreadLocal?
InheritableThreadLocal es una subclase que copia el valor del hilo padre al crear un hilo hijo. ThreadLocal no propaga nada a los hilos nuevos. La herencia puede servir para propagar contexto, pero multiplica el riesgo de fugas de memoria porque los valores se copian a cada descendiente.
¿Cómo funciona ThreadLocal con virtual threads?
Los virtual threads de Project Loom soportan ThreadLocal y restauran los valores al reanudar un hilo suspendido. El problema es el volumen: con millones de virtual threads, los datos pesados se multiplican. La recomendación es usar ThreadLocal con moderación y limpiar los valores al terminar.
Artículos Relacionados
Newsletter Semanal de Java
Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.
🔒 Sin spam. Cancela cuando quieras.