1. Hilos clásicos: cómo funciona y dónde duele
Recordemos primero cómo funcionan los hilos normales en Java (también llamados de plataforma o native threads). Esos que se crean con new Thread(...).
Cuando llamas a new Thread(() -> { ... }).start();, la JVM no se limita a ejecutar un trozo de código. Le pide al sistema operativo crear un hilo real de ejecución. El SO le asigna su propia pila (normalmente varios megabytes) y reserva otros recursos de sistema.
Ese hilo vive mientras se ejecuta su tarea y, durante todo ese tiempo, ocupa espacio en la tabla de hilos del sistema operativo. Cuantos más hilos de este tipo haya, más memoria se consume en sus pilas y mayor es la carga para el SO. Por eso, con una gran cantidad de hilos trabajando a la vez, la aplicación puede empezar a «ahogarse»: el sistema invierte demasiado tiempo en cambiar entre ellos.
Ejemplo: hilo clásico
Thread thread = new Thread(() -> {
System.out.println("¡Hola desde el hilo!");
});
thread.start();
Parece sencillo, ¿verdad? Pero intenta crear no uno o dos, sino, digamos, diez mil hilos así, y tu programa empezará a ahogarse rápidamente. O se acabará la memoria, o el sistema dirá que el límite de hilos se ha agotado. No es un fallo de Java, sino una consecuencia natural de la arquitectura: los hilos son pesados y caros.
¿Por qué ocurre esto? Cada hilo recibe su propia pila (normalmente 1–2 megabytes), además de todo un conjunto de estructuras de servicio del sistema operativo. Y al propio SO no le entusiasma que le presenten decenas de miles de hilos: tiene límites, y a veces son bastante estrictos.
E incluso si no se ha acabado la memoria, surge otro problema: la conmutación de contexto. Cuando hay demasiados hilos, el sistema salta constantemente entre ellos, guardando y restaurando su estado. Todo eso consume tiempo y rendimiento, así que el «paralelismo masivo» no da el resultado esperado.
El problema de «un hilo — una petición»
En aplicaciones de servidor antiguas (por ejemplo, en Tomcat o Jetty) se solía aplicar el modelo «thread‑per‑request»: para cada petición entrante del usuario se asignaba un hilo aparte. Es cómodo, pero si tienes 10_000 usuarios, ¡necesitas 10_000 hilos! El servidor se vuelve pesado y empieza la carrera por la memoria, no por la velocidad de procesamiento.
Resultado:
Los hilos clásicos son buenos para un número pequeño de tareas en paralelo, pero no escalan hasta decenas o cientos de miles.
2. ¿Qué son los hilos virtuales (Virtual Threads)?
Aquí es donde aparece el protagonista de la lección de hoy: los hilos virtuales. No es «otro hilo más», sino una idea arquitectónica completamente distinta.
Los hilos virtuales son hilos gestionados no por el sistema operativo, sino por la propia JVM. Están implementados completamente dentro de Java y pueden crearse en cantidades enormes (decenas y cientos de miles) sin «inflar» la memoria ni provocar ralentizaciones.
En breve:
- Platform Thread (hilo de plataforma): hilo normal que corresponde directamente a un hilo del SO.
- Virtual Thread (hilo virtual): hilo ligero gestionado por la JVM, no por el SO.
¿Cómo está construido?
Los hilos virtuales son hilos «ligeros» que no viven en el sistema operativo, sino dentro de la propia JVM. Funcionan sobre un pequeño pool de hilos reales, llamados platform threads. Se puede imaginar que la JVM los gestiona como un director de orquesta: tiene un número limitado de músicos (hilos reales), pero reparte hábilmente entre ellos las partituras (hilos virtuales).
Esquema arquitectónico:
+-------------------+ +-------------------+
| Virtual Thread 1 |---\ | Platform Thread |
| Virtual Thread 2 |---->====> | (Carrier Thread) |
| Virtual Thread 3 |---/ +-------------------+
... (Sistema operativo)
Carrier Thread es un hilo normal del SO en el que la JVM ejecuta muchos hilos virtuales. Si algún hilo virtual se bloquea de repente —por ejemplo, esperando datos del disco o de la red—, la JVM simplemente lo «congela», liberando el carrier thread para otras tareas.
¿Por qué es revolucionario?
Porque ahora se puede escribir código habitual y lineal —sin infinitos callbacks, CompletableFuture ni cadenas «infernalmente» largas de thenApply— y, aun así, escalar la aplicación a miles de operaciones simultáneas.
Los hilos virtuales ocupan solo decenas de kilobytes (en lugar de megabytes en los normales) y se crean prácticamente al instante. Por eso se pueden lanzar y destruir por miles sin temer que el sistema operativo se venga abajo bajo su peso. Esto hace que la programación paralela en Java sea, por fin, fácil y natural.
3. Ventajas de los hilos virtuales
Escalabilidad
Con hilos virtuales puedes permitirte el lujo de lanzar decenas y cientos de miles de tareas en paralelo. Por ejemplo, procesar cada petición de red en un hilo aparte sin preocuparte de que el servidor «reviente».
Demostración: 100 000 hilos virtuales
for (int i = 0; i < 100_000; i++) {
Thread.ofVirtual().start(() -> {
// Aquí puede haber cualquier lógica
try {
Thread.sleep(1000); // Simulación de trabajo
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
System.out.println("¡Todos los hilos se han iniciado!");
¡Este código se ejecuta sin problemas en un portátil normal!
Intenta hacer lo mismo con hilos normales y verás un OutOfMemoryError o convertirás tu ordenador en un «ladrillo».
Simplicidad de programación
Los hilos virtuales permiten escribir código «bloqueante» habitual sin convertirlo en «espagueti» de llamadas asíncronas. Por ejemplo, puedes usar tranquilamente Thread.sleep, InputStream.read, Socket.accept: la JVM se encargará de no bloquear todo el carrier thread.
Mejora de la legibilidad y el mantenimiento del código
En lugar de esquemas complejos con callbacks y CompletableFuture, escribes código lineal y comprensible. Esto reduce los bugs y facilita el mantenimiento.
No hace falta reinventar la rueda
Antes, para procesar miles de peticiones en paralelo, había que usar frameworks asíncronos, bibliotecas reactivas (Netty, Vert.x, Project Reactor), que exigen un estilo de programación particular. Ahora puedes prescindir de ellos y aun así obtener escalabilidad.
4. Arquitectura: cómo funcionan los hilos virtuales «bajo el capó»
Mapeo a carrier threads
La JVM crea un pequeño pool de hilos reales (carrier threads), normalmente tantos como núcleos tenga el procesador. Todos los hilos virtuales «viajan» sobre esos carrier threads como pasajeros en autobuses.
- Cuando un hilo virtual se bloquea (por ejemplo, esperando una respuesta de la red), la JVM lo «descarga» del carrier thread y lo pone en cola.
- En cuanto el hilo puede continuar, la JVM lo vuelve a «subir» a un carrier thread libre.
Analogía:
Imagina que tienes 4 taxis (carrier threads) y atiendes a 10 000 clientes (virtual threads). En cuanto un cliente llega a su destino y baja, el taxi recoge al siguiente. Nadie está parado y los taxis no se rompen por el peso de los pasajeros.
Planificación y conmutación
La JVM decide de forma autónoma qué hilo virtual ejecutar en cada momento. Si un hilo se bloquea en I/O, no impide que otros hilos sigan trabajando.
5. Limitaciones y particularidades de los hilos virtuales
No todo lo virtual es oro
No para cómputo prolongado: Si tienes una tarea que consume CPU de forma continua (heavy CPU‑bound), un hilo virtual no aportará mejora de rendimiento. Sencillamente porque los carrier threads siguen limitados por el número de núcleos.
Algunos bloqueos no son eficaces: Los mecanismos de sincronización antiguos (por ejemplo, sincronización con synchronized sobre objetos con mutexes nativos) pueden impedir que la JVM «congele» un hilo virtual. En esos casos el carrier thread esperará junto con el hilo virtual, reduciendo la escalabilidad.
No todas las bibliotecas se llevan bien con los hilos virtuales: Si una biblioteca hace llamadas nativas o usa bloqueos específicos, los hilos virtuales pueden no comportarse como se espera.
Ejemplo: cuándo no conviene usar Virtual Threads
Si tienes una tarea que gira en un bucle infinito y cuenta números, un hilo virtual no dará ninguna ventaja. Seguimos limitados por el número de núcleos.
Thread.ofVirtual().start(() -> {
while (true) {
// Contamos hasta el infinito
}
});
Resultado:
Un carrier thread estará ocupado con este hilo virtual y otras tareas esperarán su turno.
6. Comparación: Platform Thread vs Virtual Thread
| Característica | Platform Thread (Normal) | Virtual Thread (Virtual) |
|---|---|---|
| Gestionado por | Sistema operativo | JVM |
| Memoria por hilo | Megabytes | Decenas de kilobytes |
| Número de hilos | Normalmente < 10_000 | Miles, cientos de miles |
| Coste de creación | Costoso | Barato |
| Escalabilidad | Limitada | Casi ilimitada |
| Adecuado para | Tareas largas, CPU‑bound | Tareas cortas, I/O‑bound |
| Conmutación de hilos | SO | JVM |
| Compatibilidad | 100% | Casi siempre, pero con matices |
7. Ejemplo: cómo sería un servidor antes y después de Virtual Threads
Antes (Platform Threads)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
new Thread(() -> handleClient(client)).start();
}
Problema:
Tras 5 000 conexiones, el servidor empezará a «ahogarse».
Después (Virtual Threads, Java 21+)
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
Thread.ofVirtual().start(() -> handleClient(client));
}
Magia:
Ahora se pueden manejar decenas de miles de conexiones, ¡sin pensar en los límites de hilos!
9. Errores típicos al migrar a hilos virtuales
Error n.º 1: Esperar aceleración en tareas de cómputo. Los hilos virtuales no aceleran tareas que cargan por completo la CPU. Para ese tipo de tareas seguimos limitados por el número de núcleos.
Error n.º 2: Usar sincronizaciones bloqueantes antiguas. Si usas bloqueos antiguos (por ejemplo, synchronized sobre objetos que pueden quedar «bloqueados» nativamente), los hilos virtuales pueden no descargarse del carrier thread, perdiendo sus ventajas.
Error n.º 3: No considerar el comportamiento de bibliotecas de terceros. Algunas bibliotecas de terceros pueden no estar preparadas para trabajar con hilos virtuales (por ejemplo, si usan JNI o bloqueos nativos).
Error n.º 4: Esperar un crecimiento mágico del rendimiento. Los hilos virtuales no son una panacea. No aceleran todo, solo hacen que el paralelismo sea barato y cómodo para tareas I/O‑bound.
GO TO FULL VERSION