Java moderno: concurrencia con virtual threads y CompletableFuture (Java 21+)

java Java moderno: concurrencia con virtual threads y CompletableFuture (Java 21+)

Java moderno: concurrencia con virtual threads y CompletableFuture (Java 21+)

En este artículo vas a ver cómo aprovechar virtual threads para simplificar código concurrente en Java. El ejemplo práctico realiza varias peticiones HTTP en paralelo usando HttpClient y un executor de virtual threads. Requisitos: Java 21+ (o una versión de Java que incluya virtual threads y el helper Executors.newVirtualThreadPerTaskExecutor()).

Por qué virtual threads

Las virtual threads permiten escribir código bloqueante tradicional (por ejemplo, llamadas HttpClient.send()) sin pagar el coste de crear miles de hilos del sistema. Son ligeras y gestionadas por la JVM, lo que hace la programación concurrente mucho más sencilla que con callbacks o frameworks reactivos cuando la mayor parte del trabajo es I/O.

Ejemplo práctico: peticiones HTTP concurrentes

Este ejemplo lanza peticiones a varias URLs en paralelo y recoge los resultados de forma sencilla y clara.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

public class VirtualThreadHttp {
    public static void main(String[] args) throws Exception {
        List<String> urls = List.of(
            "https://httpbin.org/delay/1",
            "https://httpbin.org/delay/2",
            "https://example.com"
        );

        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(3))
            .build();

        // try-with-resources para asegurarnos de cerrar el executor
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<CompletableFuture<HttpResponse<String>>> futures =
                urls.stream()
                    .map(url -> CompletableFuture.supplyAsync(() -> {
                        HttpRequest req = HttpRequest.newBuilder(URI.create(url))
                            .timeout(Duration.ofSeconds(5))
                            .GET()
                            .build();
                        try {
                            // send() bloquea; con virtual threads eso está bien
                            return client.send(req, BodyHandlers.ofString());
                        } catch (Exception e) {
                            throw new CompletionException(e);
                        }
                    }, executor))
                    .collect(Collectors.toList());

            // espera a que todas acaben
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

            // procesar resultados con manejo de errores por petición
            for (int i = 0; i < urls.size(); i++) {
                try {
                    HttpResponse<String> resp = futures.get(i).join();
                    System.out.printf("%s -> %d (len=%d)\n", urls.get(i), resp.statusCode(), resp.body().length());
                } catch (CompletionException ce) {
                    System.err.printf("%s -> ERROR: %s\n", urls.get(i), ce.getCause());
                }
            }
        }
    }
}

Qué hace el código y por qué

  • Usamos Executors.newVirtualThreadPerTaskExecutor() para ejecutar cada tarea en una virtual thread. La API proporciona un ExecutorService que puedes usar con CompletableFuture u otras APIs que acepten un Executor.
  • Las llamadas a client.send(...) son bloqueantes, pero al ejecutarlas en virtual threads no bloqueas hilos del sistema. El código se mantiene secuencial y fácil de razonar.
  • En caso de excepción envolvemos con CompletionException para que CompletableFuture propague correctamente el error.
  • Se usan timeouts en el HttpRequest y en el HttpClient para evitar hilos eternamente bloqueados por redes lentas.

Buenas prácticas y riesgos

  • No confundas virtual threads con una bala de plata: si tu tarea es intensiva en CPU, sigue usando pools limitados de hilos de plataforma (p. ej. ForkJoinPool.commonPool() o un ThreadPoolExecutor configurado). Las virtual threads reducen la latencia en I/O masivo, pero no aceleran cómputo puro.
  • Cuidado con llamadas nativas que puedan bloquear la plataforma (synchronized en bibliotecas nativas, operaciones JNI que no soportan virtual threads): esas pueden “anclar” una virtual thread a un carrier thread y limitar escalabilidad.
  • Evita fuga de recursos: usa try-with-resources para cerrar executors y sockets. No crees ejecutores ilimitados sin control en bucles que procesan entradas externas.
  • Evita abusar de los ThreadLocals: si hay muchos hilos ligeros, ThreadLocal con grandes estructuras puede consumir memoria inesperada.
  • Valida y sanitiza URLs y parámetros externos para evitar SSRF u otras vulnerabilidades si las peticiones usan datos no confiables.

Si necesitas control fino de cancelación y manejo de fallos en un conjunto de subtareas, considera usar Structured Concurrency (Java 21+). Structured concurrency te ayuda a cancelar todas las tareas cuando una falla y a agrupar el manejo de errores en un solo bloque lógico; su API te permite escribir código más robusto para flujos compuestos de tareas concurrentes.

Consejo avanzado: en producción mide siempre la latencia tail (p50/p95/p99) y el uso de GC cuando pases de cientos a miles de solicitudes concurrentes. Ajusta timeouts, limita las operaciones nativas y separa las rutas CPU-bound en pools dedicados. Si vas a exponer esta técnica en servicios públicos, añade validación estricta de entradas y límites (rate limiting, circuit breakers) para minimizar el riesgo de DoS y SSRF.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión