Un servidor web atiende muchas peticiones a la vez. En Tomcat, cada petición la procesa un hilo; entender cómo se reparten y reutilizan es necesario para decidir qué estado puede guardarse y dónde.
Tomcat mantiene un pool de hilos con hilos worker disponibles. Cuando llega una petición HTTP, se asigna a un hilo libre; al terminar la respuesta, el hilo vuelve al pool y puede atender otra petición.
| Peticiones | En qué hilos se ejecutan |
|---|---|
| Simultáneas | Cada una se ejecuta en un hilo distinto |
| Sucesivas | Pueden ejecutarse en el mismo hilo reutilizado |
Crear un hilo por cada petición no es viable, porque crear un hilo es costoso:
Por eso el servidor crea los hilos una vez y los reutiliza: ese conjunto de hilos preparados es el pool. Al terminar la petición, el hilo no muere: vuelve al pool y espera la siguiente. En los logs de Tomcat se reconocen por su nombre (http-nio-8080-exec-1, http-nio-8080-exec-2…): un mismo nombre atiende peticiones distintas porque el hilo se reutiliza.
El pool se describe con dos parámetros: cuántos hilos hay preparados desde el principio (mínimo) y hasta dónde puede crecer bajo demanda (máximo). En Spring Boot, que arranca Tomcat embebido, se configura en application.properties:
server.tomcat.threads.min-spare=10 # mínimo: hilos preparados desde el arranque server.tomcat.threads.max=200 # máximo: límite superior del pool
Con los valores por defecto:
Si los 200 hilos están ocupados, las peticiones nuevas esperan en cola hasta que se libera un hilo; si la cola también se llena, la petición se rechaza.
Cada petición retiene su hilo todo el tiempo que dura el procesamiento. Si el código pasa ese rato esperando a la base de datos o a una API externa, el hilo queda bloqueado mientras tanto: no atiende ninguna otra petición. Con el pool copado de peticiones lentas, peticiones rápidas que llegan después se quedan en cola aunque su trabajo sea corto. Por eso la duración de las peticiones condiciona el tamaño que necesita el pool.
Un campo static pertenece a la clase: todos los hilos ven y modifican el mismo valor.
public class SharedValue {
private static volatile String value;
public static void setValue(String newValue) {
value = newValue;
}
public static String getValue() {
return value;
}
}
Si dos peticiones fijan valores distintos a la vez, la segunda puede sobrescribir el valor antes de que la primera lo lea: una petición trabaja con el valor de otra. Es una condición de carrera: el resultado depende del orden temporal en que se ejecutan los hilos.
volatile no crea un valor distinto para cada hilo: el campo static sigue siendo único y compartido. La diferencia es qué garantía hay al leer escrituras de otros hilos:
Hilo A: escribe "A" Hilo B: escribe "B" Hilo A: lee el campo
volatile: si la escritura de B ocurre antes de la lectura de A, A ve “B”. La escritura es visible, pero el campo sigue siendo compartido.volatile: Java no garantiza que A vea la escritura reciente de B. Puede observar un valor antiguo; no se puede asumir que verá “A” ni que cada hilo conserve un valor propio.
En ambos casos hay acceso compartido. volatile garantiza visibilidad, pero no evita que una escritura sobrescriba otra ni hace indivisible una secuencia de operaciones. Por tanto, no evita la condición de carrera.
Campo static | Variable local |
|---|---|
| Un único valor visible para todos los hilos | Cada hilo tiene la suya en su propia pila |
| Requiere cuidado ante accesos concurrentes | No se comparte entre peticiones |
ThreadLocal guarda un valor distinto para cada hilo: cada hilo solo ve el suyo.
public class ThreadData {
private static final ThreadLocal<String> DATA_BY_THREAD = new ThreadLocal<>();
public static void setData(String data) {
DATA_BY_THREAD.set(data);
}
public static String getData() {
return DATA_BY_THREAD.get();
}
public static void clearData() {
DATA_BY_THREAD.remove();
}
}
Pero el valor es por hilo, no por petición. Como Tomcat reutiliza los hilos, el valor fijado durante una petición sigue ahí cuando el mismo hilo atiende la siguiente: la nueva petición encontraría un valor que no le corresponde. Es una fuga de datos entre peticiones.
La solución es eliminar el valor al terminar la petición con remove(), en un bloque finally para que se ejecute siempre:
try {
// procesar la petición usando ThreadData.getData()
} finally {
ThreadData.clearData();
}
ThreadLocal durante una petición debe eliminarse al terminarla. Si el hilo se reutiliza, el valor se filtra a la siguiente petición.
spring.threads.virtual.enabled=true.