Hilos y ExecutorService
Crear new Thread() para cada tarea es caro y difícil de controlar. Un ExecutorService gestiona un pool de hilos reutilizables: le envías tareas con submit y recibes un Future con el resultado. Al terminar hay que cerrarlo.
En una línea ExecutorService pool = Executors.newFixedThreadPool(8); Future<Informe> f = pool.submit(() -> generar(id));
List<Long> ids = List.of(1L, 2L, 3L, 4L);
// Java 17: cierre manual
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
List<Future<Informe>> futuros = pool.invokeAll(
ids.stream().<Callable<Informe>>map(id -> () -> generador.generar(id)).toList(),
30, TimeUnit.SECONDS); // las que no acaben a tiempo se cancelan
for (Future<Informe> f : futuros) {
try {
guardar(f.get());
} catch (ExecutionException e) {
log.error("Falló un informe", e.getCause());
} catch (CancellationException e) {
log.warn("Informe cancelado por timeout");
}
}
} finally {
pool.shutdown();
if (!pool.awaitTermination(10, TimeUnit.SECONDS)) pool.shutdownNow();
}
// Java 21: el executor es AutoCloseable (close() espera a que terminen las tareas)
try (var vt = Executors.newVirtualThreadPerTaskExecutor()) {
ids.forEach(id -> vt.submit(() -> generador.generar(id)));
}
Cómo funciona por dentro
Un hilo de plataforma es un hilo del sistema operativo: reserva memoria para su pila (del orden de 1 MB) y crearlo y destruirlo cuesta. Por eso no se crea uno por tarea, sino que se reutilizan en un pool. Executors.newFixedThreadPool(n) crea n hilos y una cola para las tareas pendientes; submit encola un Callable o un Runnable y devuelve un Future, e invokeAll envía varias tareas y espera a todas.
Future.get() bloquea hasta tener el resultado y relanza cualquier excepción de la tarea envuelta en ExecutionException; usa get(timeout, unidad) para no esperar indefinidamente. Si nadie llama a get, la excepción de una tarea enviada con submit se pierde en silencio.
El tamaño del pool depende del trabajo: para tareas de CPU, cerca del número de núcleos (Runtime.getRuntime().availableProcessors()); para tareas que esperan I/O, más, pero acotado por lo que aguanta el recurso compartido, como el pool de conexiones. Al terminar se llama a shutdown() y awaitTermination(); en Java 21, el executor es AutoCloseable y un try-with-resources lo hace por ti. Un newCachedThreadPool sin límite puede crear miles de hilos bajo carga y tumbar el proceso.
@Async, en el servidor web y en los listeners de mensajería.
ExecutorService implementa AutoCloseable: en Java 21 puedes usarlo en un try-with-resources. En Java 17 hay que llamar a shutdown() y awaitTermination().
Se conecta con Pools de hilosFutureVirtual threads