Hilos Virtuales en Java: Escalabilidad sin Complejidad
El problema de la concurrencia tradicional
Java ha dependido históricamente de hilos de plataforma para la concurrencia. Cada hilo de plataforma es un wrapper del hilo del sistema operativo, con un mapeo 1:1. Eso significa que cada hilo se trae entre 1 y 2 MB de memoria solo por existir, más los recursos del OS que consume.
El problema es que el modelo "un hilo por petición" (thread-per-request), que es el que usan la mayoría de servidores web, no escala. Si recibes 10.000 peticiones concurrentes, necesitas 10.000 hilos, y ahí es donde te encuentras con OutOfMemoryError o saturas el context switching del procesador. La solución tradicional ha sido programación reactiva con Project Reactor o RxJava, que mejora el throughput pero a costa de un código que es un infierno de leer, depurar y mantener.
¿Qué son los hilos virtuales?
Project Loom introdujo los hilos virtuales, que se estabilizaron en JDK 21 (JEP 444). La idea es simple: hilos ligeros gestionados por la JVM que cuestan ~1 KB cada uno en vez de 1-2 MB.
La consecuencia directa es que puedes crear millones de hilos virtuales sin agotar memoria. Están optimizados para tareas I/O-bound (peticiones HTTP, queries de base de datos, operaciones de ficheros), no para trabajo CPU-bound. Y lo mejor: son un drop-in replacement del código Thread existente. No tienes que reescribir nada, cambiar el paradigma, ni aprender una API nueva. Son el equivalente de Java a las goroutines de Go.
Han estado estables desde JDK 21 y siguen mejorando en JDK 22, 23, 24 y 25.
Funcionamiento interno: M:N scheduling
El modelo es M:N. M hilos virtuales (potencialmente millones) se ejecutan sobre N hilos de plataforma (los núcleos de CPU disponibles).
Cuando un hilo virtual se monta en un carrier (un hilo de plataforma) y ejecuta código, todo va normal. Pero cuando el hilo virtual hace una operación bloqueante como sleep(), un socket read, o una query de base de datos, se desmonta del carrier. El carrier queda libre para que la JVM lo asigne a otro hilo virtual. Cuando la operación I/O termina, el hilo virtual vuelve al scheduler y se remonta en cualquier carrier disponible.
Esto maximiza la utilización de CPU porque los carriers siempre están trabajando, nunca esperando.
Dos actualizaciones recientes merecen atención. JDK 24 (JEP 491) eliminó el pinning en bloques synchronized, lo que significa que los hilos virtuales ahora se desmontan correctamente incluso dentro de secciones sincronizadas. Y JDK 25 (JEP 506) trajo Scoped Values como alternativa inmutable a ThreadLocal, que previene los memory leaks que aparecen cuando usas millones de hilos.
Ejemplos prácticos
Los hilos virtuales brillan en escenarios I/O-bound.
Hilo virtual simple
Thread.startVirtualThread(() -> {
System.out.println("¡Hola desde HV! " + Thread.currentThread());
});ExecutorService para alta concurrencia
import java.util.concurrent.Executors;
import java.time.Duration;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 10k tareas concurrentes
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // Simula I/O
// Lógica business
})
);
} // Auto-awaits completionEsto lanza 10.000 tareas concurrentes con un solo executor. Con hilos de plataforma tradicionales necesitarías un pool cuidadosamente dimensionado. Aquí no: cada tarea recibe su propio hilo virtual y la JVM hace el resto.
Spring Boot con hilos virtuales
En benchmarks recientes con JDK 24, Spring Boot y Postgres, los hilos virtuales redujeron la latencia aproximadamente un 50% frente a hilos de plataforma manejando 10.000 peticiones por segundo.
@RestController
public class ApiController {
@GetMapping("/data")
String getData() {
// HV maneja request automáticamente en Tomcat/Undertow con config
return fetchFromDb(); // Bloquea pero escala
}
}Consideraciones avanzadas y mejores prácticas
La distinción más importante es entre tareas I/O-bound y CPU-bound. Las tareas I/O-bound (acceso a base de datos, llamadas HTTP) se benefician enormemente de los hilos virtuales con newVirtualThreadPerTaskExecutor(), porque la mayoría del tiempo están esperando y el carrier queda libre. Las tareas CPU-bound (criptografía, machine learning) no ganan nada con hilos virtuales y pueden causar carrier starvation: si todos los carriers están ocupados haciendo cómputo, los hilos virtuales no consiguen tiempo de ejecución. Para CPU-bound, usa un pool fijo de hilos de plataforma del tamaño de tus núcleos.
El pinning era el problema más conocido de los hilos virtuales en sus primeras versiones: dentro de un bloque synchronized, el hilo no podía desmontarse, lo que anulaba el beneficio. JDK 24 lo resolvió. Aun así, conviene evitar JNI y código nativo, que siguen causando pinning.
ThreadLocal es peligroso con hilos virtuales. Si creas un millón de hilos y cada uno tiene su copia de un ThreadLocal, la memoria explota. Scoped Values (JDK 25) es la solución: son inmutables, se heredan correctamente y no acumulan estado entre hilos.
Para monitoreo, Thread.currentThread().isVirtual() te dice si estás en un hilo virtual. JDK Flight Recorder soporta hilos virtuales desde JDK 21, y Spring Boot 3.2+, Quarkus y Helidon los soportan de forma nativa.
Mi recomendación para 2025: usa hilos virtuales por defecto en servidores web. Profilea con JDK Mission Control o JFR para detectar pinning residual. Migra ThreadLocal a Scoped Values cuando puedas. Mantén pools fijos de hilos de plataforma para cómputo puro. Y si estás pensando en más de 100.000 conexiones concurrentes, prueba con wrk o ab antes de subir a producción.
Si estás empezando, usa Thread.startVirtualThread() para todo lo concurrente sin pensarlo dos veces. Si ya eres experto, úsalos para I/O pero mantén los hilos de plataforma para CPU puro, y vigila el pinning.
La metáfora que me ayuda a recordarlo: los hilos de plataforma son ascensores dedicados, uno por persona. Caros, limitados. Los hilos virtuales son un sistema de tubos neumáticos: miles de cápsulas ligeras circulando por pocos tubos físicos.
Fuentes: OpenJDK JEPs, benchmarks Spring Boot (Reddit/Java 24), guías RockTheJVM/RabinaNPatra (2025).