Executorcompletionservice Y Futuretask

ExecutorCompletionService y FutureTask

1. ExecutorCompletionService

Es una clase que implementa el patrón Decorador para ExecutorService, optimizando el manejo de resultados de tareas asíncronas. Su función principal es retornar los resultados de las tareas tan pronto como estén disponibles, en lugar de esperar a que todas finalicen.

  • Explicación

    ¿Qué significa que ExecutorCompletionService implementa el patrón Decorador para ExecutorService?

    1. Patrón Decorador (Decorator Pattern):

    • Es un patrón de diseño que añade funcionalidad a un objeto existente sin modificar su estructura, “envolviéndolo” (wrapping).
    • Ejemplo clásico: Agregar capas de funcionalidad a un objeto (como agregar toppings a una pizza sin cambiar su base).

    2. Aplicado a ExecutorCompletionService:

    • ExecutorCompletionService no es un ExecutorService en sí mismo, sino que envuelve uno existente (como ThreadPoolExecutor o ForkJoinPool).
    • Extiende su comportamiento para agregar la capacidad de recuperar resultados de tareas en el orden en que completan, no en el que se enviaron.

    ¿Cómo optimiza el manejo de resultados asíncronos?

    Problema Tradicional con ExecutorService:

    Si envías múltiples tareas a un ExecutorService y guardas los Futures en una lista:

    ExecutorService executor = Executors.newFixedThreadPool(2);
    List<Future<String>> futures = new ArrayList<>();
    
    futures.add(executor.submit(() -> tareaLenta())); // Tarea 1 (5 segundos)
    futures.add(executor.submit(() -> tareaRapida())); // Tarea 2 (1 segundo)
    
    // Para obtener resultados:
    for (Future<String> future : futures) {
        String resultado = future.get(); // Espera 5s por Tarea 1, aunque Tarea 2 ya terminó
    }
    • Inconveniente: El bucle for procesa los resultados en el orden de envío, no de finalización. Aunque la Tarea 2 termina primero, debes esperar a la Tarea 1.

    Solución con ExecutorCompletionService:

    ExecutorService executor = Executors.newFixedThreadPool(2);
    ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executor);
    
    completionService.submit(() -> tareaLenta()); // Tarea 1 (5s)
    completionService.submit(() -> tareaRapida()); // Tarea 2 (1s)
    
    for (int i = 0; i < 2; i++) {
        Future<String> future = completionService.take(); // Retorna el FUTURE que complete primero
        String resultado = future.get();
        System.out.println(resultado);
    }
    • Resultado:

      Resultado de Tarea 2 (1s)
      Resultado de Tarea 1 (5s)

    ¿Cómo funciona internamente?

    1. Decoración:
      • ExecutorCompletionService recibe un ExecutorService (ej: un ThreadPool).
      • Cada tarea enviada (submit()) es envuelta en un FutureTask personalizado.
    2. Cola de Completados:
      • Cuando una tarea finaliza, su Future se agrega automáticamente a una cola bloqueante (como LinkedBlockingQueue).
    3. Método take():
      • Retira el Future de la cola en el orden de finalización (no de envío).
      • Si la cola está vacía, bloquea el hilo hasta que haya un resultado disponible.

    Ventajas Clave

    1. Eficiencia en Tiempo:
      • Procesas resultados tan pronto están listos, sin esperar a tareas más lentas.
      • Ejemplo útil: Si tienes 10 tareas (9 rápidas y 1 lenta), procesas las 9 rápidas inmediatamente.
    2. Evita Bloqueos Innecesarios:
      • No necesitas llamar a future.get() en orden secuencial, lo que reduciría la ejecución a un proceso sincrónico.
    3. Manejo Simplificado:
      • No requieres estructuras complejas para rastrear qué tareas han completado.

    Ejemplo de Mundo Real

    Imagina un servidor que procesa solicitudes de usuarios:

    • Tarea 1: Generar un reporte complejo (30 segundos).
    • Tarea 2: Validar un formulario (100 ms).

    Con ExecutorCompletionService:

    • La respuesta del formulario (Tarea 2) se envía al cliente en 100 ms, sin esperar a que el reporte (Tarea 1) termine.

    Conclusión

    ExecutorCompletionService es una herramienta poderosa para escenarios donde:

    • Las tareas tienen tiempos de ejecución variables.
    • Necesitas procesar resultados tan pronto estén disponibles.
    • Quieres maximizar la eficiencia en entornos de alto rendimiento o paralelismo.

Ejemplo de Uso:

ExecutorService executor = Executors.newCachedThreadPool();
ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executor);

// Enviar tareas al servicio
completionService.submit(() -> "Tarea 1");
completionService.submit(() -> "Tarea 2");

// Recuperar resultados en el orden de finalización
for (int i = 0; i < 2; i++) {
    Future<String> future = completionService.take(); // Bloquea hasta que haya un resultado
    System.out.println(future.get());
}

¿Qué hace el método take()?:

  • Retorna el Future de la siguiente tarea que se complete, sin importar el orden en que fueron enviadas.
  • Si no hay resultados disponibles, bloquea el hilo actual hasta que una tarea finalice.

Ventajas:

  • Eficiencia: Maneja los resultados a medida que llegan, evitando esperas innecesarias (ideal para tareas con tiempos variables de ejecución).
  • Simplificación del código: Elimina la necesidad de iterar manualmente sobre una lista de Futures y usar isDone().
  • Orden de finalización: Prioriza procesar primero las tareas que terminan más rápido.
  • Integración con ExecutorService: Funciona con cualquier implementación de ExecutorService (ThreadPool, ForkJoinPool, etc.).

2. FutureTask

Es una clase que implementa las interfaces Runnable y Future, permitiendo crear tareas ejecutables que pueden ser manejadas como Futures. Su método done() se ejecuta automáticamente cuando la tarea finaliza (éxito o error), lo que permite manejar resultados de forma reactiva.

Ejemplo de Uso:

public class MiFutureTask extends FutureTask<String> {
    public MiFutureTask(Callable<String> callable) {
        super(callable);
    }

    @Override
    protected void done() {
        try {
            System.out.println("Resultado: " + get()); // Solo se ejecuta si la tarea completó
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

// Uso en código
ExecutorService executor = Executors.newCachedThreadPool();
MiFutureTask tarea1 = new MiFutureTask(() -> "Resultado 1");
MiFutureTask tarea2 = new MiFutureTask(() -> "Resultado 2");

executor.submit(tarea1);
executor.submit(tarea2);

Ventajas:

  • Manejo reactivo: El método done() permite ejecutar lógica personalizada al completarse la tarea (ej: notificaciones, logging).
  • Encapsulación: Combina la definición de la tarea (Runnable/Callable) y el manejo del resultado en una sola clase.
  • Flexibilidad: Útil para escenarios donde se requiere acción inmediata tras la finalización, sin necesidad de llamar explícitamente a get().

¿Por qué usar ExecutorCompletionService en lugar de FutureTask directamente?

  • AbstracciónExecutorCompletionService ya encapsula la lógica de FutureTask y la cola de resultados, simplificando el código.
  • Manejo de múltiples tareas: Es más eficiente para procesar resultados de muchas tareas en paralelo, ya que no requiere crear una subclase por cada tarea.
  • API más limpia: Métodos como take() o poll() facilitan el acceso a los resultados sin necesidad de hooks personalizados.

Flujo de Trabajo Conjunto

Ambas clases pueden usarse juntas para lograr un manejo eficiente y reactivo:

  1. ExecutorCompletionService para recuperar resultados en orden de finalización.
  2. FutureTask si se requiere lógica específica al completar una tarea (ej: actualizar una UI o enviar un mensaje).

Mejores Prácticas

  1. Cierre seguro de recursos: Usar try-with-resources con ExecutorService para garantizar que los hilos se detengan correctamente.
  2. Evitar get() innecesario: En FutureTask, el método done() ya maneja el resultado, por lo que llamar a get() puede ser redundante.
  3. Priorizar ExecutorCompletionService: Para la mayoría de casos de procesamiento paralelo, es más eficiente que manejar Futures manualmente.

Conclusión

  • ExecutorCompletionService es ideal para procesar resultados de tareas en paralelo de forma eficiente.
  • FutureTask es útil cuando se necesita reacción inmediata a la finalización de una tarea específica.
  • Ambas clases mejoran la legibilidad y eficiencia del código asíncrono en Java.