4 - Executor y ExecutorService

Los ejecutores encapsulan pools de hilos reutilizables y una cola de tareas. Permiten controlar el paralelismo sin crear y destruir hilos a mano, reduciendo latencias y fuga de recursos.

4.1 Por qué usar ejecutores

  • Pool reutilizable: los hilos se crean una vez y atienden muchas tareas (un pool es un grupo gestionado de hilos precreados).
  • Evita creación/destrucción constante: menos overhead de CPU y memoria.
  • Control del paralelismo: defines cuántos hilos pueden trabajar en paralelo.
  • Gestín simplificada: la cola interna ordena tareas y evita que el código de negocio gestione hilos manualmente.

4.2 Creación de pools

  • Executors.newFixedThreadPool(n): tamaño fijo; bueno para CPU-bound cuando n ≈ núcleos.
  • Executors.newCachedThreadPool(): crece bajo demanda, reutiliza hilos inactivos; útil para I/O-bound variable.
  • Executors.newSingleThreadExecutor(): un solo hilo, garantiza orden de ejecución.
  • Executors.newScheduledThreadPool(n): agenda tareas periódicas o con retraso.
  • Diferencias: el fijo limita concurrencia; el cached puede crecer mucho si hay picos; el single es serial; el scheduled agrega temporizadores.

4.3 Envío de tareas a un ExecutorService

  • submit(Runnable): ejecuta sin retorno.
  • submit(Callable): ejecuta devolviendo un Future con el valor.
  • invokeAll(): lanza una colección de Callable y espera todos los resultados.
  • invokeAny(): devuelve el resultado del primer Callable exitoso y cancela el resto.
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class DemoExecutor {
  public static void main(String[] args) throws Exception {
    ExecutorService pool = Executors.newFixedThreadPool(2);

    Callable<String> t1 = () -> {
      TimeUnit.MILLISECONDS.sleep(300);
      return "Tarea 1";
    };
    Callable<String> t2 = () -> {
      TimeUnit.MILLISECONDS.sleep(100);
      return "Tarea 2";
    };

    List<Future<String>> resultados = pool.invokeAll(Arrays.asList(t1, t2));
    for (Future<String> f : resultados) {
      System.out.println(f.get());
    }

    String primero = pool.invokeAny(Arrays.asList(t1, t2));
    System.out.println("Primero en terminar: " + primero);

    pool.shutdown();
  }
}

4.4 Ciclo de vida del ExecutorService

  • shutdown(): deja de aceptar nuevas tareas y espera que terminen las pendientes.
  • shutdownNow(): intenta interrumpir hilos y devuelve tareas no iniciadas.
  • awaitTermination(): bloquea hasta que el pool termina o vence un timeout.
  • Importancia: cerrar siempre libera hilos y evita fugas que impiden detener la JVM.

4.5 Problemas frecuentes

  • Pools mal dimensionados: pocos hilos generan colas grandes; demasiados hilos compiten por CPU y memoria.
  • Thread starvation: pools muy chicos pueden bloquearse si tareas esperan resultados dentro del mismo pool.
  • Tareas que nunca finalizan: bloqueos o bucles infinitos agotan el pool.
  • Deadlocks cruzados: tareas en el pool esperando otras tareas del mismo pool; usa pools separados o diseña sin dependencias circulares.