5 - Hilos Virtuales (Project Loom) en Java 21+

Los hilos virtuales (Project Loom) son hilos ligeros administrados por la JVM que se multiplexan sobre un pool reducido de hilos plataforma. Permiten manejar muchas conexiones I/O-bound con código bloqueante y legible, sin pagar el costo de miles de hilos nativos.

5.1 ¿Qué es un hilo virtual?

  • Hilo ligero: gestionado por la JVM; se aparca y reanuda sin bloquear un hilo nativo.
  • Multiplexado: muchos hilos virtuales comparten pocos hilos plataforma (carrier threads).
  • Semántica de hilo: mantiene el modelo de hilos de Java (stack, synchronized, APIs heredadas).

5.2 Comparación con los hilos plataforma

  • Costo de creación: virtual << plataforma (casi constante, sin stack grande inicial).
  • Bloqueo: las esperas de I/O aparcan el hilo virtual, liberando el hilo nativo.
  • Planificación: la JVM decide cuándo montar/desmontar virtuales sobre carriers.

5.3 Creación de hilos virtuales (Thread y Executor)

  • Thread.ofVirtual().start(runnable) crea y lanza un hilo virtual.
  • Executors.newVirtualThreadPerTaskExecutor() genera un executor que crea un hilo virtual por tarea.
  • Programación estructurada: el código bloqueante sigue luciendo imperativo.
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class HilosVirtualesIntro {
  public static void main(String[] args) throws Exception {
    Thread.ofVirtual().start(() -> System.out.println("Hola desde hilo virtual"));

    try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
      exec.submit(() -> {
        Thread.sleep(200);
        System.out.println("Tarea virtual: " + Thread.currentThread());
        return null;
      }).get();
    }
  }
}

El factory Thread.ofVirtual() produce hilos ligeros al vuelo. El executor per-task crea un hilo virtual por cada submit y se cierra automáticamente en el try-with-resources.

5.4 Bloqueo con I/O y aparcado

  • Aparcado (park): al llamar a Socket.read() o Thread.sleep(), el hilo virtual se aparca; el carrier queda libre.
  • Reanudación: cuando hay datos, el virtual se monta de nuevo en un carrier y sigue ejecutando.
  • Impacto: miles de hilos pueden esperar I/O sin saturar hilos nativos.

5.5 Impacto sobre pools y programación estructurada

  • Pools simples: un executor per-task reemplaza la afinación de pools fijos/caché para tareas I/O-bound.
  • Menos callbacks: se favorece código secuencial bloqueante en lugar de cadenas complejas de futures.
  • ThreadLocal: sigue funcionando, pero hay que considerar la creación masiva de hilos.

5.6 Cuándo usarlos (I/O-bound extremo)

  • Conexiones masivas: servidores con muchas sockets o peticiones HTTP simultáneas.
  • Trabajo bloqueante: llamadas a BD, servicios externos, colas remotas.
  • Legibilidad: mantener estilo imperativo y estructurado sin romper el flujo con callbacks.

5.7 Cuándo NO usarlos (CPU-bound, contención alta)

  • Cálculo puro: tareas CPU-bound siguen limitadas por el número de cores; usa pools fijos dimensionados.
  • Locks pesados: contención en locks o secciones críticas largas reduce el beneficio.
  • Operaciones no bloqueantes: si no hay I/O ni esperas, no aportan ventaja.

5.8 Relación con sincronización

  • synchronized: funciona igual; pero un bloqueo largo frena el hilo virtual y todo el carrier, evitando aparcar.
  • Locks y atomics: siguen vigentes; preferir secciones críticas cortas para maximizar aparcado.
  • APIs bloqueantes: mientras la llamada sea "loom-friendly" (coopera con aparcado), la sincronización es transparente.

5.9 Interoperabilidad con APIs antiguas y legado

  • Bibliotecas preexistentes: la mayoría de I/O bloqueante tradicional coopera; pero librerías con bloqueos nativos pueden fijar el carrier.
  • Thread pools existentes: se pueden mezclar virtuales y plataforma; evita pasar virtuales a pools que los "pinchen".
  • Diagnóstico: usa jcmd (p.ej. jcmd <pid> Thread.print -format=json) o banderas de trazas de Loom (-Djdk.tracePinnedThreads=full) para detectar pinning y ver dónde se fijan los hilos virtuales.

5.10 Servidor web simple usando virtual threads

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ServidorVirtual {
  public static void main(String[] args) throws Exception {
    HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
    ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor();
    server.createContext("/saludo", new SaludoHandler());
    server.setExecutor(exec); // un hilo virtual por request
    server.start();
    System.out.println("Escuchando en http://localhost:8080/saludo");
  }

  static class SaludoHandler implements HttpHandler {
    public void handle(HttpExchange exchange) throws IOException {
      String body = "Hola desde un hilo virtual: " + Thread.currentThread().toString();
      byte[] data = body.getBytes(StandardCharsets.UTF_8);
      exchange.sendResponseHeaders(200, data.length);
      try (var os = exchange.getResponseBody()) {
        os.write(data);
      }
    }
  }
}

El servidor usa un executor de hilos virtuales: cada request obtiene su propio hilo ligero, y las esperas de I/O (lectura/escritura) aparcan sin retener carriers. Perfecto para muchas conexiones concurrentes sin tunear pools.

Diagrama de servidor con hilos virtuales

5.11 Rendimiento: millones de conexiones

  • Escala en I/O: se han reportado cientos de miles o millones de sockets con memoria y planificador razonables.
  • Memoria: el stack se expande bajo demanda; el costo inicial por hilo es minúsculo.
  • Planificador cooperativo: depende de puntos seguros de aparcado; código que fija el carrier reduce el beneficio.

5.12 Limitaciones actuales y futuro de Loom

  • Pinning: se produce cuando un lock o código nativo impide aparcar; evita secciones sincronizadas largas.
  • Depuración: stack traces masivos pueden ser más ruidosos; usa filtros y labels en hilos.
  • Estabilidad: seguir las notas de versión; algunas APIs nativas o drivers pueden no estar optimizados.