1. Cómo crear hilos virtuales en la práctica
¡Hora de pasar de la teoría a la práctica! Ya sabes cómo se crea un hilo a la manera clásica:
Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();
O un poco más corto:
new Thread(() -> System.out.println("Hi!")).start();
Ahora, con Java 21, tenemos una nueva forma:
Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));
o de forma más explícita:
Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));
¿Cuál es la diferencia?
- Thread.ofVirtual().start(...) crea un hilo virtual (Virtual Thread), gestionado por la JVM y no por el SO.
- Thread.ofPlatform().start(...) (o new Thread(...)) — un hilo de plataforma clásico, como antes.
¿Por qué es importante?
Se pueden crear hilos virtuales por decenas de miles sin temor a OutOfMemoryError. Ahora, si de repente decides procesar un millón de peticiones, Java dirá: «¡Sin problema, trae más!»
2. Sintaxis para crear un hilo virtual
Ejemplo básico:
public class VirtualThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.ofVirtual().start(() -> {
System.out.println("¡Hola desde un hilo virtual! Hilo: " + Thread.currentThread());
});
// Esperamos a que termine el hilo (para que main no finalice antes)
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
¿Qué está pasando?
- Creamos un hilo virtual mediante Thread.ofVirtual().start(...).
- Dentro del hilo hay una acción sencilla: imprimir un mensaje.
- Al final llamamos a thread.join(), para que el hilo principal espere a que finalice el virtual (de lo contrario, el programa podría terminar antes de que el hilo alcance a imprimir algo).
Atención:
Un hilo virtual se ve y se comporta casi como uno normal, pero por dentro — ¡magia de la JVM!
3. Creación masiva de hilos virtuales: la fuerza de Loom en la práctica
Y ahora probemos algo que con hilos normales sería arriesgado (o simplemente imposible): vamos a crear 10_000 hilos virtuales, cada uno de los cuales imprimirá su número.
public class VirtualThreadMassive {
public static void main(String[] args) throws InterruptedException {
int N = 10_000;
Thread[] threads = new Thread[N];
for (int i = 0; i < N; i++) {
int threadNum = i;
threads[i] = Thread.ofVirtual().start(() -> {
System.out.println("Hilo virtual #" + threadNum + " en funcionamiento!");
});
}
// Esperamos a que terminen todos los hilos
for (Thread t : threads) {
t.join();
}
System.out.println("¡Todos los hilos virtuales han terminado!");
}
}
- Para los hilos clásicos (new Thread(...)), este código casi con total seguridad hará que tu programa se caiga con OutOfMemoryError.
- Para los hilos virtuales, ¡este es el modo normal! La JVM gestionará con facilidad miles y decenas de miles de hilos.
Por cierto, si te parece que 10_000 es mucho, prueba 100_000 o incluso 1_000_000. En una máquina moderna, la JVM se las arreglará si tus hilos realizan un trabajo sencillo o están esperando E/S.
4. Runnable y expresiones lambda: cómo pasar código a un hilo virtual
Los hilos virtuales aceptan tareas igual que los hilos normales: mediante la interfaz Runnable. Esto significa que puedes pasar expresiones lambda, una referencia a método o cualquier objeto que implemente Runnable.
Ejemplo con lambda:
Thread.ofVirtual().start(() -> System.out.println("¡Lambda en un hilo virtual!"));
Ejemplo con método:
public class TaskRunner {
public static void main(String[] args) {
Thread.ofVirtual().start(TaskRunner::doWork);
}
static void doWork() {
System.out.println("Trabajando en un hilo virtual: " + Thread.currentThread());
}
}
Ejemplo con clase anónima:
Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println("¡Clase anónima en un hilo virtual!");
}
});
Conclusión:
Todo lo que funcionaba con hilos normales también funciona con los virtuales — solo que ahora es «fácil y rápido».
5. Comparación con ExecutorService: enfoque antiguo y nuevo
ExecutorService clásico
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Tarea #" + taskNum + " en ejecución");
});
}
executor.shutdown();
Problema:
Si hay demasiadas tareas y pocos hilos — las tareas esperan en la cola. Si hay demasiados hilos — el programa se «asfixiará» por falta de recursos.
Nuevo enfoque: Executor sobre hilos virtuales
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
int taskNum = i;
executor.submit(() -> {
System.out.println("Tarea virtual #" + taskNum);
});
}
executor.shutdown();
¿Qué ocurre?
- Se crea un hilo virtual independiente por cada tarea.
- La JVM gestiona su planificación, sin sobrecargar el sistema.
- No hace falta limitar el tamaño del pool — los hilos virtuales son «casi gratis».
¿Cuándo usar un Executor con hilos virtuales?
- Cuando necesites lanzar tareas en masa (por ejemplo, procesar un gran número de peticiones, archivos o conexiones de red).
- Cuando las tareas son independientes y pueden ejecutarse en paralelo.
- Cuando quieras integrar hilos virtuales en una arquitectura existente en la que ya se use ExecutorService (por ejemplo, en un servidor web, un procesador de tareas, etc.).
Consejo:
Si no estás seguro — empieza con un Executor de hilos virtuales. Es la forma más versátil y moderna.
6. Consejos prácticos: cuándo usar cada cosa
¿Cuándo usar Thread.ofVirtual().start() directamente?
- Cuando necesites crear un hilo aparte para una tarea única (por ejemplo, para una prueba, una demo o un experimento sencillo).
- Cuando el número de hilos es pequeño y quieres gestionarlos manualmente.
¿Cuándo usar Executors.newVirtualThreadPerTaskExecutor()?
- Cuando necesites lanzar tareas en masa (por ejemplo, procesar un gran volumen de peticiones, archivos o conexiones de red).
- Cuando las tareas son independientes y no requieren coordinación entre sí.
- Cuando quieras integrar hilos virtuales en una arquitectura existente donde ya se use ExecutorService (por ejemplo, en un servidor web, un procesador de tareas, etc.).
Consejo:
Si no estás seguro — empieza con un Executor de hilos virtuales. Es la forma más versátil y moderna.
7. Manejo de excepciones en hilos virtuales
Los hilos virtuales son hilos normales desde el punto de vista de try-catch. Si dentro de tu Runnable se produce una excepción, no «romperá» toda la JVM; simplemente finalizará ese hilo con error.
Ejemplo:
Thread t = Thread.ofVirtual().start(() -> {
throw new RuntimeException("¡Algo ha salido mal!");
});
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("El hilo principal siguió ejecutándose.");
En un ExecutorService:
Si envías una tarea mediante submit, puedes obtener el resultado con Future y la excepción se propagará al invocar get():
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
throw new RuntimeException("Error en la tarea virtual");
});
try {
f.get();
} catch (ExecutionException e) {
System.out.println("Capturamos el error del hilo virtual: " + e.getCause());
}
executor.shutdown();
8. Errores típicos al crear hilos virtuales
Error n.º 1: confundir hilos virtuales y de plataforma. Si creas hilos con new Thread(...) o Thread.ofPlatform(), no son hilos virtuales. Solo Thread.ofVirtual().start(...) o los métodos de Executors te dan hilos virtuales de verdad.
Error n.º 2: esperar aceleración en cómputo pesado. Los hilos virtuales no aceleran tareas intensivas de CPU (CPU-bound). Si tienes un millón de hilos y cada uno calcula Pi hasta el millonésimo dígito — la JVM no podrá «acelerar» el cálculo; solo conmutará entre hilos.
Error n.º 3: mantener recursos (por ejemplo, bases de datos) por cada hilo. Si creas un millón de hilos virtuales, pero cada uno necesita una conexión independiente a la base, la base no lo soportará. Los hilos virtuales son adecuados para tareas donde la mayor parte del tiempo es de espera (E/S), no para trabajar con recursos externos limitados.
Error n.º 4: no esperar a que terminen los hilos cuando es importante. Si el hilo principal termina antes que los hilos virtuales — el programa puede finalizar sin esperar los resultados. Usa join() o ExecutorService con shutdown() y awaitTermination().
Error n.º 5: usar bibliotecas obsoletas no compatibles con hilos virtuales. Algunas bibliotecas de terceros pueden bloquear hilos a nivel del SO o usar sincronización nativa, lo que reduce la eficacia de los hilos virtuales. Comprueba siempre la compatibilidad.
GO TO FULL VERSION