1. Escalabilidad
¿Por qué los hilos tradicionales escalan mal?
Cada hilo clásico (Thread) es una entidad del sistema operativo con su propia pila (normalmente 1–2 MB) y estructura de estado. El intento de crear, digamos, 10 000 hilos normales a menudo conduce al error OutOfMemoryError. Por eso en los servidores tradicionales se usan pools de hilos limitados.
Hilos virtuales: la magia de la escalabilidad
Los hilos virtuales (Java 21+) son hilos “ligeros” gestionados por la JVM. Su pila se almacena en el heap y puede crecer/encogerse dinámicamente. Cuando un hilo se bloquea en I/O, la JVM lo “congela” y continúa con otras tareas.
Dentro de la JVM funciona un pequeño pool de «portadores» (carrier threads), hilos de plataforma del SO sobre los que se van ejecutando los hilos virtuales. Esto permite crear 100 000+ tareas sin pánico por la memoria. La JVM planifica automáticamente qué hilos virtuales ejecutar en cada momento.
Demostración: 100_000 hilos virtuales frente a 1_000 hilos de plataforma
Ejemplo: creación de 1000 hilos normales
// Intento de crear 1000 hilos normales
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads.add(t);
t.start();
}
System.out.println("Hilos creados: " + threads.size());
Resultado: En la mayoría de los sistemas se podrán crear 1 000–2 000 hilos; con valores mayores empezarán los problemas de memoria y las ralentizaciones.
Ejemplo: creación de 100 000 hilos virtuales
// Creamos 100_000 hilos virtuales
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
vThreads.add(t);
}
System.out.println("Hilos virtuales creados: " + vThreads.size());
Resultado: El programa crea sin problemas 100 000 hilos virtuales sin caídas ni ralentizaciones significativas. Se requiere mucha menos memoria.
Comparación visual
| Tipo de hilo | Máximo de hilos (aprox.) | Uso de memoria | Tiempo de arranque |
|---|---|---|---|
| Tradicionales (Thread) | 1 000 – 10 000 | Alto | Lento |
| Virtuales | 100 000 – 1 000 000+ | Bajo | Instantáneo |
Hecho: Los hilos virtuales permiten escribir código de «un hilo por tarea» sin pools complejos ni riesgo de sobrecargar el sistema.
2. Rendimiento: dónde los hilos virtuales se lucen
Tareas limitadas por E/S (I/O‑bound)
Los hilos virtuales son ideales para solicitudes de red, I/O de archivos y trabajo con bases de datos. Cuando la operación se bloquea, el hilo virtual libera al «portador» y la JVM ejecuta otras tareas. Esto aumenta el rendimiento con un gran número de esperas simultáneas.
Ejemplo: simulación de 10 000 solicitudes HTTP simultáneas
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
Thread t = Thread.ofVirtual().start(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Respuesta: " + response.statusCode());
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
});
threads.add(t);
}
// Esperamos a que terminen todos los hilos
for (Thread t : threads) {
t.join();
}
Resultado: Las 10 000 solicitudes se ejecutan en paralelo; el programa no se cae y el código sigue siendo sencillo.
CPU‑bound: los hilos virtuales no aceleran los cálculos
Si la tarea carga la CPU, los hilos virtuales no añadirán velocidad: el número de núcleos es fijo. Aquí convienen pools fijos, iguales al número de núcleos, para no crear competencia innecesaria.
// Cada tarea calcula la suma de un rango grande
Runnable cpuTask = () -> {
long sum = 0;
for (int i = 0; i < 100_000_000; i++) {
sum += i;
}
System.out.println("Suma: " + sum);
};
// Lanzamos 1000 hilos virtuales con cálculos
for (int i = 0; i < 1000; i++) {
Thread.ofVirtual().start(cpuTask);
}
Resultado: Los hilos competirán por la CPU, pero no habrá aceleración: no es una tarea para Virtual Threads.
3. Limitaciones y particularidades
Sincronización y trampas de los hilos virtuales
- Cuidado con los bloqueos nativos. El uso de synchronized puede “pegar” un hilo virtual al portador, reduciendo la ganancia. Prefiere ReentrantLock, Semaphore y otros primitivos de java.util.concurrent optimizados para hilos virtuales.
- Bibliotecas antiguas. Algunos drivers JDBC y bibliotecas nativas aún no están optimizados para Virtual Threads. Prueba cuidadosamente las operaciones bloqueantes.
No para tareas de larga duración
Virtual Threads es ideal para unidades de trabajo “cortas”: procesar una solicitud, una operación y fin. Un millón de tareas eternas (por ejemplo, cálculos sin fin) no aportarán beneficios; para ellas usa hilos de plataforma.
4. Mejores prácticas: dónde usar hilos virtuales
- Tareas I/O‑bound: llamadas de red, archivos, BBDD — todo donde el hilo espera a menudo.
- Servidores web: procesa cada solicitud HTTP en un hilo virtual separado.
- Pruebas de integración: simula miles de clientes rápidamente.
- Procesamiento asíncrono: escribe código “bloqueante” habitual — la JVM realiza una planificación inteligente.
No conviene usarlos:
- Para tareas que cargan continuamente la CPU.
- Cuando la compatibilidad con bibliotecas de bajo nivel es crítica (aún no todas están adaptadas).
Bajo el capó es cómodo usar el ejecutor: Executors.newVirtualThreadPerTaskExecutor() — «un hilo virtual por tarea», sin pool fijo.
5. Supervisión y medición: cómo ver los hilos virtuales en acción
JVisualVM y Flight Recorder
JVisualVM mostrará los hilos activos, sus estados y la memoria; desde Java 21 los hilos virtuales se muestran por separado. Java Flight Recorder (JFR) registra una detallada “caja negra” de la ejecución, incluida la estadística de Virtual Threads — útil para encontrar cuellos de botella.
Cómo ver la cantidad de hilos en el código
Una forma sencilla de ver la cantidad de hilos en la JVM:
System.out.println("Total de hilos: " + Thread.activeCount());
Para contar cuántos de ellos son virtuales:
long vCount = Thread.getAllStackTraces().keySet().stream()
.filter(Thread::isVirtual)
.count();
System.out.println("Hilos virtuales: " + vCount);
6. Errores típicos al trabajar con hilos virtuales
Error n.º 1: usar hilos virtuales para cálculos pesados. Lanzar millones de hilos virtuales con tareas CPU‑bound no acelerará el procesador. Virtual Threads no es un “turbo” para los cálculos.
Error n.º 2: copiar a ciegas los patrones antiguos. No crees pools fijos de hilos virtuales. Usa Executors.newVirtualThreadPerTaskExecutor() y deja que la JVM escale automáticamente.
Error n.º 3: usar bibliotecas no compatibles. Los bloqueos nativos y las bibliotecas no adaptadas a Loom pueden provocar bloqueos y caídas de rendimiento. Comprueba la compatibilidad con antelación.
Error n.º 4: optimización prematura. Si tienes unos pocos hilos y multihilo convencional, no te apresures a migrarlo todo a Virtual Threads. Es una herramienta útil donde hay I/O masivo y espera.
Error n.º 5: ignorar la supervisión. Crear un millón de tareas es fácil, pero sin monitoreo y manejo de excepciones puedes obtener un “benchmark bonito” en lugar de un sistema fiable. Usa JVisualVM y JFR.
GO TO FULL VERSION