- Los hilos virtuales son un mecanismo de hilos ligeros lanzado oficialmente en JDK 21, gestionados por la Máquina Virtual de Java (JVM). Utilizan un modelo de mapeo M:N (múltiples hilos virtuales reutilizan unos pocos hilos portadores del sistema operativo). Su principle ventaja es el bajo costo de creación (memoria de pila inicial de solo unos pocos KB), soportan concurrencia a nivel de millones y son ideales para tareas intensivas en E/S.
- Diferencia clave: en comparación con los hilos de plataforma tradicionales (que usan un modelo 1:1 vinculado a hilos del SO), los hilos virtuales liberan automáticamente el hilo portador al ejecutar operaciones de bloqueo, sin necesidad de configurar manualmente parámetros del pool de hilos, y son totalmente compatibles con la sintaxis de programación síncrona y el código Java existente.
- Los hilos virtuales de Java son, en esencia, hilos ligeros en modo usuario implementados a nivel de JVM. Junto con las gorutinas de Go, las corrutinas de Kotlin y las de Python (asyncio), pertenecen a las "primitivas de concurrencia ligera". Sin embargo, las diferencias clave radican en el modelo de implementación, la estrategia de planificación, el paradigma de programación y el grado de vinculación al lenguaje. Las diferencias principales se resumen a continuación:
- Hilos virtuales de Java: planificación preventiva a nivel de JVM, sin necesidad de llamar manualmente a
yield; compatibles con la sintaxis síncrona de Java. - Gorutinas de Go: planificación preventiva gestionada por el runtime de Go, fuertemente vinculadas al lenguaje.
- Corrutinas de Kotlin/Python: planificación cooperativa a nivel de biblioteca, requieren llamadas manuales a
awaitoyield; el costo de adaptación del código es alto.
Planificación de Hilos Virtuales en Java
Planificación de Gorutinas (Corrutinas) en Go
Comparativa: Hilo Virtual vs. Corrutinas Principales
| Dimensión | Hilo Virtual (Java) | Gorutina (Go) | Corrutina (Kotlin) | Corrutina (Python asyncio) |
|---|---|---|---|---|
| Nivel de implementación | JDK (modo usuario) + planificación JVM, desacoplado del SO | Planificación del runtime de Go, desacoplado del SO | Biblioteca del lenguaje (basado en pool de hilos / funciones suspendidas) | Biblioteca del lenguaje (bucle de eventos + callbacks) |
| Estrategia de planificación | Preventiva (JVM planifica activamente, sin yield manual) |
Preventiva (runtime de Go planifica activamente) | Cooperativa (requiere suspend/resume manual) |
Cooperativa (requiere await manual) |
| Modelo de mapeo | M:N (hilo virtual → hilo portador → hilo SO) | M:N (gorutina → P → M → hilo SO) | 1:N (corrutina → pool de hilos) | 1:1 (corrutina → bucle de eventos → un solo hilo) |
| Manejo de bloqueo | Liberación automática (al bloquear E/S, se cede el hilo portador) | Liberación automática (al bloquear E/S, se cede el hilo M) | Requiere await manual (de lo contrario bloquea el pool) |
Obligatorio await (de lo contrario bloquea el bucle de eventos) |
| Paradigma de programación | Síncrono (sin modificar código, compatible con APIs existentes) | Síncrono (palabra clave go + código síncrono) |
Paradigma mixto (funciones suspendidas + launch/async) |
Asíncrono (palabras clave async/await) |
| Vinculación al lenguaje | Débilmente acoplado con Java (compatible con todo código JVM) | Fuertemente vinculado a Go (exclusivo del runtime Go) | Fuertemente vinculado a Kotlin (sintaxis de funciones suspendidas) | Fuertemente vinculado a Python (sintaxis async/await) |
| Modelo de pila | Pila expansible (asignación dinámica, inicial ~KB) | Pila expansible (inicial 2KB, crecimiento dinámico) | Corrutina sin pila (basada en máquina de estados) | Corrutina sin pila (basada en generadores) |
| Manejo de excepciones | Compatible con excepciones tradicionales de Java (try/catch) |
Requiere manejo explícito (recover) |
Excepciones estructuradas (CoroutineExceptionHandler) |
Requiere try/except envolviendo await |
| Límite de concurrencia | Millones | Millones | Decenas de miles (limitado por el pool de hilos) | Miles por proceso (limitado por el bucle de eventos) |
Explicación de Diferencias Clave y Ejemplos de Código
Estrategia de Planificación: Preventiva vs. Cooperativa (Diferencia Esencial)
- Esta es la diferencia más fundamental entre los hilos virtuales/gorutinas y las corrutinas de Python/Kotlin:
- Hilos virtuales de Java / Gorutinas de Go: planificación preventiva — el runtime (JVM/Go runtime) interrumpe activamente tareas que llevan mucho tiempo con la CPU, cediendo el control sin intervención del desarrollador.
- Corrutinas de Python/Kotlin: planificación cooperativa — se debe llamar manualmente a
await/yieldpara ceder el control; de lo contrario, una única corrutina puede bloquear todo el hilo/bucle de eventos.
- Ejemplo 1: Hilo virtual de Java (preventivo, código síncrono)
// Sin palabras clave especiales, el código síncrono soporta alta concurrencia automáticamente
public class DemoHiloVirtual {
public static void main(String[] args) {
try (var ejecutor = Executors.newVirtualThreadPerTaskExecutor()) {
// Envía 1000 tareas, sin await, la JVM planifica automáticamente
for (int i = 0; i < 1000; i++) {
final int id = i;
ejecutor.submit(() -> {
// Simula bloqueo de E/S (cede automáticamente el hilo portador)
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Ejecutando en hilo virtual: " + Thread.currentThread() + ", tarea " + id);
});
}
}
}
}
- Ejemplo 2: Corrutina Python asyncio (cooperativa, obligatorio
await)
import asyncio
async def tarea(i):
# Debe usar asyncio.sleep (no time.sleep) + await, de lo contrario bloquea el bucle
await asyncio.sleep(1)
print(f"Corrutina ejecutada: {i}")
async def principal():
# Crear lista de tareas manualmente + await
tareas = [asyncio.create_task(tarea(i)) for i in range(1000)]
await asyncio.gather(*tareas)
# Iniciar el bucle de eventos
asyncio.run(principal())
- Diferencia clave:
- Hilo virtual de Java:
Thread.sleep(1000)es una operación bloqueante, pero la JVM descarga automáticamente el hilo virtual del hilo portador, permitiendo que otros hilos virtuales se ejecuten. - Corrutina Python: si se usa
time.sleep(1)(en lugar deasyncio.sleep(1)), se bloquea directamente el bucle de eventos y todas las corrutinas se detienen.
- Hilo virtual de Java:
Paradigma de Programación: Compatibilidad Síncrona vs. Refactorización Asíncrona
- La mayor ventaja de los hilos virtuales en Java es que son totalmente compatibles con el código síncrono existente, sin necesidad de modificar la lógica de negocio. En cambio, otras corrutinas (excepto Go) requieren una refactorización significativa:
- Hilos virtuales Java: el código existente con
Thread/ThreadPoolExecutorsolo necesita reemplazarse porExecutors.newVirtualThreadPerTaskExecutor(); la lógica de negocio no requiere cambios. - Corrutinas Kotlin: es necesario convertir funciones normales en funciones suspendidas (
suspend fun) y llamarlas dentro de un ámbito de corrutina (CoroutineScope). - Corrutinas Python: todas las operaciones de E/S deben cambiarse a versiones asíncronas (por ejemplo,
aiohttpen lugar derequests) y rodearse conasync/await.
- Hilos virtuales Java: el código existente con
- Ejemplo: Costo de adaptación en Kotlin
// Función normal (no se puede usar directamente con corrutinas)
fun tareaSincrona() {
Thread.sleep(1000) // Bloquea el hilo
}
// Debe convertirse en función suspendida
suspend fun tareaAsincrona() {
delay(1000) // Retardo de corrutina (no bloqueante)
}
// Debe llamarse dentro de un ámbito de corrutina
fun main() = runBlocking {
launch { tareaAsincrona() } // Inicia una corrutina
}
- Nivel de implementación: VM vs. Biblioteca
- Hilos virtuales Java / Gorutinas Go: implementados a nivel de VM/runtime, débilmente acoplados al lenguaje (los hilos virtuales Java pueden usarse en Scala, Groovy, etc.).
- Corrutinas Kotlin/Python: implementadas como bibliotecas, fuertemente dependientes de la sintaxis del lenguaje (ej.
suspenden Kotlin,async/awaiten Python), no reutilizables entre lenguajes.
- Modelo de pila: Con pila vs. Sin pila
- Hilos virtuales Java / Gorutinas Go: corrutinas con pila — cada corrutina tiene su propia pila de llamadas, soportan anidamiento ilimitado y son compatibles con código síncrono tradicional.
- Corrutinas Kotlin/Python: corrutinas sin pila — implementadas mediante máquinas de estados, la información de la pila se almacena en variables, el anidamiento requiere una cadena estricta de
await, menor flexibilidad.
Complemento: Similitudes Clave (Evitar Confusiones)
Todas las primitivas de concurrencia ligera comparten un objetivo común: reducir el costo de recursos en la programación concurrente y aumentar el rendimiento en escenarios de alta concurrencia. Las similitudes incluyen:
- Todas se ejecutan en modo usuario; el costo de creación, destrucción y cambio es mucho menor que el de los hilos del sistema operativo.
- Todas son adecuadas para tareas intensivas en E/S (las tareas intensivas en CPU aún dependen del paralelismo multinúcleo).
- Todas pueden soportar una concurrencia mucho mayor que los hilos del SO (miles/millones).
- Analogía: Roles de las cuatro "unidades de concurrencia"
| Tipo | Analogía | Característica Principal |
|---|---|---|
| Hilo virtual Java | "Trabajador con sala de descanso": el hilo portador es el puesto de trabajo; el hilo virtual es el trabajador; cuando va a beber agua (se bloquea), cede el puesto a otro trabajador. | Compatible con código antiguo; al bloquearse en E/S, libera recursos automáticamente. |
| Gorutina Go | "Trabajador en cadena de montaje": el runtime es la cadena; asigna trabajadores a diferentes puestos automáticamente; incluso si un trabajador se distrae, es reasignado. | Soporte nativo, planificación preventiva, extremadamente eficiente. |
| Corrutina Python asyncio | "Trabajador por turnos": debe pedir permiso (await) para cambiar de turno; de lo contrario, ocupa el puesto. |
Planificación cooperativa, requiere bibliotecas asíncronas. |
| JS Promise/async | "Recepcionista": solo hay un recepcionista (hilo principal); los clientes (tareas) hacen cola; primero se atienden las sincrónicas y luego se llama al número. | Simula corrutinas, sin pausa/reaunudación real. |
Comparativa con JDK 1.8: Ventajas Clave y Mejora de Rendimiento
- JDK 1.8 solo soporta hilos de plataforma tradicionales y el mecanismo de pool de hilos (
ThreadPoolExecutor). Los hilos virtuales (JDK 21+) ofrecen mejoras significativas en capacidad de concurrencia, utilización de recursos y eficiencia de desarrollo. La comparativa y ventajas de rendimiento son las siguientes:
- Comparativa de ventajas clave (hilos virtuales JDK 21+ vs. hilos tradicionales JDK 1.8)
- Uso de recursos: los hilos tradicionales JDK 1.8 tienen memoria de pila fija (1-2 MB por defecto), con alto costo de creación y destrucción. Los hilos virtuales asignan memoria de pila bajo demanda (solo unos pocos KB inicialmente), con un costo de creación aproximadamente mil veces menor, soportando millones de hilos concurrentes, muy por encima del límite de miles de los pools tradicionales.
- Gestión de hilos: JDK 1.8 requiere configurar manualmente parámetros del pool (tamaño del núcleo, máximo, cola de tareas, política de rechazo, etc.), lo que puede provocar agotamiento del pool, acumulación de tareas o desperdicio de recursos. Los hilos virtuales no requieren configuración manual; la JVM gestiona automáticamente la planificación de los hilos portadores, reduciendo costos de desarrollo y ajuste.
- Manejo de bloqueo: en JDK 1.8, cuando un hilo tradicional se bloquea (por ejemplo, esperando E/S), ocupa el hilo del SO, dejando recursos inactivos. Los hilos virtuales, al bloquearse, liberan automáticamente el hilo portador para que otros hilos virtuales lo reutilicen, aumentando la utilización del hilo portador en más del 80%.
- Eficiencia de desarrollo: para escenarios de alta concurrencia con E/S, JDK 1.8 requiere el uso de marcos asíncronos como
CompletableFutureo WebFlux, lo que genera anidamiento de callbacks (callback hell) y dificulta la depuración. Los hilos virtuales permiten usar sintaxis síncrona, sin modificar la lógica de negocio; solo es necesario reemplazar la implementación del pool para obtener rendimiento asíncrono, mejorando la eficiencia de desarrollo y mantenimiento.
- Medición de mejora de rendimiento (escenario intensivo en E/S)
Con el mismo hardware (8 núcleos, 16 GB) y las mismas tareas intensivas en E/S (consultas a base de datos combinadas con llamadas a red), la comparativa de rendimiento entre JDK 21+ (hilos virtuales) y JDK 1.8 (pool tradicional) es la siguiente:
- Capacidad de concurrencia: el pool tradicional de JDK 1.8 soporta establemente unos 2000-3000 hilos; más allá de eso, es probable que ocurra desbordamiento de memoria (OOM) o bloqueos. Los hilos virtuales pueden soportar 100,000+ hilos sin degradación significativa.
- Latencia de respuesta: con alta concurrencia (2000+ hilos), JDK 1.8 tiene una latencia en el percentil 99 de aproximadamente 500 ms. Con 100,000+ hilos virtuales, la latencia en el percentil 99 se mantiene por debajo de 100 ms, una reducción del 80%.
- Utilización de recursos: JDK 1.8 tiene una utilización de CPU de aproximadamente 30-40% (muchos hilos inactivos por bloqueo). Los hilos virtuales elevan la utilización de CPU al 70-80%, y el uso de memoria se reduce en un 60% (millones de hilos virtuales ocupan similar memoria que unos pocos miles de hilos tradicionales).
- Rendimiento (throughput): en igualdad de carga de trabajo, el rendimiento de los hilos virtuales es de 10 a 20 veces superior al del pool tradicional de JDK 1.8, especialmente en escenarios de alta frecuencia de E/S como puertas de enlace o interfaces de microservicios.
- Nota: en tareas intensivas en CPU, la diferencia entre hilos virtuales y tradicionales no es significativa; incluso puede haber una ligera sobrecarga debido al cambio de planificación, por lo que en esos casos se recomienda usar hilos de plataforma tradicionales (o el pool de JDK 1.8).
Uso Básico de Hilos Virtuales
Se recomienda priorizar el uso del pool de hilos virtuales para simplificar la gestión de concurrencia y evitar la creación manual de hilos. Los casos de uso principales y ejemplos de implementación son:
- Creación básica e inicio (escenario simple)
// Forma 1: crear e iniciar directamente un hilo virtual
Thread hiloVirtual1 = Thread.startVirtualThread(() -> {
System.out.println("Ejecutando en hilo virtual: " + Thread.currentThread());
try {
Thread.sleep(1000); // Simula bloqueo de E/S, cede automáticamente el hilo portador
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Forma 2: personalizar nombre del hilo antes de iniciar
Thread.Builder.OfVirtual constructor = Thread.ofVirtual();
Thread hiloVirtual2 = constructor.name("vt-negocio-01").unstarted(() -> {
System.out.println("Hilo virtual con nombre personalizado ejecutándose");
});
hiloVirtual2.start();
- Uso del pool de hilos virtuales (recomendado, escenario de tareas por lotes)
API principal:Executors.newVirtualThreadPerTaskExecutor(). Este método no requiere parámetros explícitos; por defecto crea un hilo virtual independiente por cada tarea enviada, sin necesidad de configurar tamaño del pool, cola, etc.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DemoPoolHilosVirtuales {
public static void main(String[] args) {
// Cierre automático del pool (try-with-resources)
try (ExecutorService ejecutor = Executors.newVirtualThreadPerTaskExecutor()) {
// Envía 10000 tareas intensivas en E/S (sin presión)
for (int i = 0; i < 10000; i++) {
int idTarea = i;
ejecutor.submit(() -> {
System.out.println("Ejecutando tarea: " + idTarea + ", hilo: " + Thread.currentThread());
try {
Thread.sleep(500); // Simula E/S (BD/red)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
}
}
- Personalización del pool de hilos virtuales (escenario especial)
Si se necesita personalizar el nombre del hilo virtual, el manejo de excepciones, etc., se puede construir unaThreadFactorypersonalizada medianteThread.ofVirtual()y luego pasarla aThreadPerTaskExecutor, logrando una configuración personalizada del pool:
// 1. Construir fábrica de hilos virtuales personalizada
ThreadFactory fabricaHilosVirtuales = Thread.ofVirtual()
.name("vt-custom-", 0) // Prefijo del nombre, con numeración automática
.uncaughtExceptionHandler((hilo, ex) -> {
System.err.println("Excepción en hilo virtual [" + hilo.getName() + "]: " + ex.getMessage());
})
.factory();
// 2. Crear pool de hilos virtuales personalizado
ExecutorService ejecutorPersonalizado = new ThreadPerTaskExecutor(fabricaHilosVirtuales);
Reglas de Compatibilidad entre Hilos Virtuales y APIs/Keywords de JDK
La relación de compatibilidad se divide en tres categorías: "totalmente compatible", "uso con restricciones" y "totalmente incompatible". Conocer los límites de cada caso ayuda a evitar riesgos técnicos.
- Totalmente compatible (uso directo)
Estas keywords y APIs no requieren modificación de código; su uso es idéntico al de los hilos tradicionales. Incluyen:
- Control de hilos:
synchronized(JDK 24+ corrigió el problema de fijación (pinning) de hilos), mecanismoswait/notify, métodojoin. - Manejo de excepciones:
try/catch/finally,throw/throws, soporte paraUncaughtExceptionHandler. - Mecanismo de interrupción:
interrupt(),isInterrupted(); los métodos bloqueantes lanzanInterruptedException. - Herramientas JUC:
CountDownLatch,Semaphore,CyclicBarrier, etc.
private static final Object CANDADO = new Object();
Thread vt = Thread.startVirtualThread(() -> {
synchronized (CANDADO) {
System.out.println("Bloqueo adquirido, ejecutando tarea");
try {
CANDADO.wait(1000); // Al bloquearse, cede automáticamente el hilo portador
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
- Uso con restricciones (requiere atención)
Estas keywords y APIs se pueden usar, pero con limitaciones explícitas:
ThreadLocal: dado el alto nivel de concurrencia de los hilos virtuales, un uso intensivo deThreadLocalpuede provocar un consumo elevado de memoria y riesgo de OOM. Además,InheritableThreadLocalno funciona correctamente (no puede heredar valores del hilo padre). Se recomienda usarScopedValue(disponible desde JDK 24) como alternativa.volatile: solo garantiza visibilidad, no afecta la planificación de hilos ni el orden de ejecución.- APIs parciales de
Thread:setDaemon(boolean)lanzaUnsupportedOperationException;setPriority(int)no tiene efecto (todos los hilos virtuales tienen la misma prioridad);setName(String)funciona, pero se recomienda usar nombres en lote para facilitar la depuración. LockSupport.park(): provoca que el hilo virtual fije (pinning) el hilo portador, anulando la ventaja principle. JDK 24+ solo corrigió las versiones con tiempo de espera (parkNanos). Se recomienda usarObject.wait()como alternativa.- Métodos nativos JNI bloqueantes: al ejecutarse, fijan el hilo portador. Se recomienda usar APIs nativas de Java en su lugar.
// Recomendado: usar ScopedValue en lugar de ThreadLocal
ScopedValue<String> sv = ScopedValue.newInstance();
ScopedValue.where(sv, "prueba").run(() -> {
Thread.startVirtualThread(() -> {
System.out.println("Valor de ScopedValue: " + sv.get());
});
});
- Totalmente incompatible (prohibido usar)
ThreadGroup: los hilos virtuales no pertenecen a ningúnThreadGroup; llamar aThread.getThreadGroup()devuelve un grupo especial que no permite gestionar hilos virtuales (interrumpirlos, detenerlos, etc.). Se recomienda usarStructuredTaskScopepara la gestión del ciclo de vida de las tareas.- APIs obsoletas:
stop(),suspend(),resume()no tienen efecto en hilos virtuales y pueden causar estados incoherentes. Usarinterrupt()oStructuredTaskScopepara cancelar tareas. - APIs que dependen de vinculación a hilos del SO: como el viejo
Selector, operaciones deProcessBuilderque requieren enlace a hilos del SO. Usar las versiones adaptadas de JDK 21+.
Puntos Clave para Evitar Problemas con Hilos Virtuales
- Adaptación al escenario: los hilos virtuales son ideales para tareas intensivas en E/S (llamadas a APIs, consultas a BD, lectura/escritura de archivos, etc.). Para tareas intensivas en CPU, se recomienda usar hilos de plataforma tradicionales o pools, para evitar la sobrecarga de cambios de contexto.
- Evitar el problema de fijación (pinning): no usar
LockSupport.park()ni métodos nativos JNI bloqueantes dentro de hilos virtuales. Actualizar a JDK 24+ para solucionar la fijación causada porsynchronized. - Control de recursos: aunque los hilos virtuales no requieren limitar manualmente la concurrencia, al interactuar con recursos externos limitados (pools de conexiones de BD, APIs de terceros), se recomienda usar
Semaphorepara limitar la concurrencia a nivel de negocio y evitar agotar los recursos. - Compatibilidad de versiones: asegurarse de que los frameworks y middleware utilizados sean compatibles con JDK 21+ (por ejemplo, Spring Boot 3.3+, Tomcat 10.1+, Quarkus, etc.) para evitar problemas de compatibilidad.
- Depuración y monitorización: utilizar herramientas adaptadas a hilos virtuales (como Arthas, Micrometer, etc.) para monitorizar el estado de millones de hilos virtuales, realizar trazabilidad de lamadas y depuración.