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
ExecutorCompletionServiceimplementa el patrón Decorador paraExecutorService?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:ExecutorCompletionServiceno es unExecutorServiceen sí mismo, sino que envuelve uno existente (comoThreadPoolExecutoroForkJoinPool).- 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
ExecutorServicey guardas losFutures 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
forprocesa 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?
- Decoración:
ExecutorCompletionServicerecibe unExecutorService(ej: un ThreadPool).- Cada tarea enviada (
submit()) es envuelta en unFutureTaskpersonalizado.
- Cola de Completados:
- Cuando una tarea finaliza, su
Futurese agrega automáticamente a una cola bloqueante (comoLinkedBlockingQueue).
- Cuando una tarea finaliza, su
- Método
take():- Retira el
Futurede 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.
- Retira el
Ventajas Clave
- 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.
- Evita Bloqueos Innecesarios:
- No necesitas llamar a
future.get()en orden secuencial, lo que reduciría la ejecución a un proceso sincrónico.
- No necesitas llamar a
- 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
ExecutorCompletionServicees 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
Futurede 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 usarisDone(). - Orden de finalización: Prioriza procesar primero las tareas que terminan más rápido.
- Integración con
ExecutorService: Funciona con cualquier implementación deExecutorService(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ón:
ExecutorCompletionServiceya encapsula la lógica deFutureTasky 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()opoll()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:
ExecutorCompletionServicepara recuperar resultados en orden de finalización.FutureTasksi se requiere lógica específica al completar una tarea (ej: actualizar una UI o enviar un mensaje).
Mejores Prácticas
- Cierre seguro de recursos: Usar
try-with-resourcesconExecutorServicepara garantizar que los hilos se detengan correctamente. - Evitar
get()innecesario: EnFutureTask, el métododone()ya maneja el resultado, por lo que llamar aget()puede ser redundante. - Priorizar
ExecutorCompletionService: Para la mayoría de casos de procesamiento paralelo, es más eficiente que manejarFutures manualmente.
Conclusión
ExecutorCompletionServicees ideal para procesar resultados de tareas en paralelo de forma eficiente.FutureTaskes ú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.