11 - Hilos en Tomcat

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

Cliente ACliente BCliente CTomcatHilo 1Hilo 2Cliente ACliente BCliente CTomcatHilo 1Hilo 2Cliente ACliente BCliente CTomcatHilo 1Hilo 2Petición Ala atiende el hilo 1Petición B (simultánea)la atiende el hilo 2respuestarespuestaPetición C (posterior)reutiliza el hilo 1respuesta

Crear un hilo por cada petición no es viable, porque crear un hilo es costoso:

  • Cada hilo de plataforma reserva memoria para su pila (en torno a 1 MB por defecto) y consume recursos del sistema operativo.
  • Crear y destruir un hilo en cada petición añade un coste temporal a todas ellas.
  • Sin límite, una avalancha de peticiones crearía miles de hilos a la vez: el servidor se agotaría por la sobrecarga antes que por falta de trabajo.

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:

  • El pool arranca con 10 hilos esperando peticiones: es el mínimo que se mantiene vivo aunque no llegue ninguna.
  • Si llegan más peticiones simultáneas que hilos libres, se crean hilos nuevos hasta el máximo (200).
  • Los hilos que quedan por encima del mínimo se retiran cuando llevan un rato sin trabajo: el pool vuelve a encogerse.

Llega una petición¿hay hilo libre?sínoSe asigna un hilo del pool¿hay menos hilos que el máximo?sínoSe crea un hilo nuevoLa petición espera en colaEl hilo procesa la peticiónEl hilo vuelve al pool

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

  • Con 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.
  • Sin 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();
}

Todo valor guardado en un ThreadLocal durante una petición debe eliminarse al terminarla. Si el hilo se reutiliza, el valor se filtra a la siguiente petición.
El modelo de hilo por petición de este tema es el que se usa en 1.º DAW. Para cargas con muchísima concurrencia existen dos alternativas, que no se ven en este curso:
  • La programación reactiva (WebFlux en Spring): en lugar de un hilo por petición, un event loop con pocos hilos atiende todas las conexiones mediante I/O no bloqueante; cuando hay que esperar a la BBDD o a una API externa, el hilo no se bloquea: deja la petición aparcada y sigue con otra. Permite mucha concurrencia con muy pocos hilos, a cambio de un código y una depuración más complejos (una sola llamada bloqueante rompe el modelo).
  • Project Loom (desde Java 21): los hilos virtuales son tan baratos de crear que se puede tener uno por petición sin preocuparse del pool: el hilo virtual se apoya en un hilo de plataforma solo mientras ejecuta código y lo libera al bloquearse en I/O. Se programa igual que siempre (código secuencial, sin callbacks) y, en Spring Boot, se activa con spring.threads.virtual.enabled=true.
  • clase/daw/prog/2eval/hilos.txt
  • Última modificación: 2026/10/13 06:29
  • por cesguiro