CodeGym /Cursos /JAVA 25 SELF /Hilos frente a hilos virtuales: diferencias y ventajas

Hilos frente a hilos virtuales: diferencias y ventajas

JAVA 25 SELF
Nivel 57, Lección 0
Disponible

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.

1
Tarea
JAVA 25 SELF, nivel 57, lección 0
Bloqueada
Inicio del mensajero secreto 💬
Inicio del mensajero secreto 💬
1
Tarea
JAVA 25 SELF, nivel 57, lección 0
Bloqueada
Preparación para el lanzamiento de una flota de drones 🚀
Preparación para el lanzamiento de una flota de drones 🚀
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION