CodeGym /Cursos /JAVA 25 SELF /ExecutorService, Callable, Future: ejecución de tareas

ExecutorService, Callable, Future: ejecución de tareas

JAVA 25 SELF
Nivel 54 , Lección 1
Disponible

1. ExecutorService: gestionamos hilos como profesionales

Por qué no conviene crear hilos simplemente con new Thread

Al principio, la programación concurrente parece sencilla:

Thread t = new Thread(() -> {
    // hacemos algo
});
t.start();

Este enfoque funciona, pero pronto se vuelve una carga cuando hay muchas tareas. Cada llamada a new Thread() crea un hilo nuevo, y decenas o cientos de hilos empiezan a sobrecargar el sistema. Además, gestionarlos es incómodo: hay que vigilar cuándo terminan, qué hacer ante errores, cómo detenerlos y reutilizarlos.

Aquí entra en escena ExecutorService, un orquestador inteligente de hilos. Tú simplemente le entregas las tareas y él decide con qué hilo y cuándo ejecutarlas. Como resultado, todo funciona más rápido, de forma más estable y sin dolores de cabeza.

Cómo funciona ExecutorService

ExecutorService funciona según un principio sencillo pero eficaz.

  • Internamente tiene un pool de hilos — un conjunto de hilos de trabajo creado de antemano (fijo o dinámico).
  • Las tareas van a una cola y son tomadas por los hilos libres.
  • El servicio gestiona el ciclo de vida: puedes esperar a que termine, detener correctamente el pool y liberar recursos.

Creación de ExecutorService

La forma más habitual — usar los métodos de fábrica de la clase Executors:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 hilos
  • newFixedThreadPool(N) — pool de N hilos (adecuado para la mayoría de tareas).
  • newCachedThreadPool() — pool dinámico, crea hilos según sea necesario (cuidado: puede agotar la memoria ante una avalancha de tareas).
  • newSingleThreadExecutor() — un solo hilo (ejecución secuencial).

Ejemplo: ejecutar Runnable mediante ExecutorService

executor.submit(() -> {
    System.out.println("¡Hola desde el pool de hilos!");
});

Cuando termines de trabajar con ExecutorService, debes cerrarlo correctamente:

executor.shutdown(); // Prohíbe añadir nuevas tareas y espera a que terminen las actuales

Importante: si no llamas a shutdown(), el programa podría no finalizar — los hilos del pool se quedarán esperando nuevas tareas.

2. Runnable vs Callable: las tareas pueden ser distintas

Antes de Java 5, si querías ejecutar algo en un hilo, escribías una implementación de la interfaz Runnable. Es una tarea que no devuelve nada y no lanza excepciones comprobadas.

Runnable task = () -> {
    System.out.println("Solo trabajo, ¡no devuelvo nada!");
};
executor.submit(task);

Callable: una tarea con resultado (y excepciones)

A veces quieres que la tarea no solo «haga algo», sino que devuelva un resultado — por ejemplo, la suma de números, el resultado de unos cálculos o datos de un servidor. Para ello existe la interfaz Callable<T>.

import java.util.concurrent.Callable;

Callable<Integer> sumTask = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
};
  • El método call() devuelve un resultado de tipo T.
  • El método call() puede lanzar una excepción comprobada.

Analogía: Runnable — «ve y lava los platos» (el resultado no importa), Callable — «ve y trae té y di a qué temperatura está» (el resultado importa).

Ejecución de Callable: para obtener el resultado, usa executor.submit(...). Devolverá un objeto Future<T>.

3. Future: una promesa de resultado

Future es una «promesa» de entregar un resultado en el futuro. Cuando envías una tarea a ExecutorService, recibes un Future, del que más tarde podrás obtener el resultado, saber si la tarea ha finalizado o cancelarla.

Métodos principales de Future

  • T get() — obtener el resultado (espera hasta que la tarea termine).
  • boolean isDone() — si la tarea ha finalizado.
  • boolean cancel(boolean mayInterruptIfRunning) — intentar cancelar la tarea.
  • boolean isCancelled() — si la tarea fue cancelada.

Ejemplo: ejecutar un Callable y obtener el resultado

import java.util.concurrent.*;

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

        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        Future<Integer> future = executor.submit(sumTask);

        System.out.println("La tarea se ha iniciado; puedes hacer algo más...");

        // Obtenemos el resultado (el método bloquea el hilo si la tarea aún no ha terminado)
        Integer result = future.get();
        System.out.println("Resultado del cálculo: " + result);

        executor.shutdown();
    }
}
  • La tarea se envía al pool de hilos.
  • Mientras la tarea se ejecuta, el hilo principal puede hacer otras cosas.
  • Cuando necesites el resultado, llama a future.get() — el hilo esperará si la tarea aún está en ejecución.
  • En cuanto la tarea termine, se devolverá el resultado.

4. Práctica: varias tareas, esperando la finalización

A menudo hay que lanzar varias tareas a la vez y esperar a que todas terminen. Por ejemplo, procesas un array de datos, lo divides en partes y calculas la suma de cada parte en una tarea aparte.

Ejemplo: sumar elementos de un array por partes

import java.util.*;
import java.util.concurrent.*;

public class ParallelArraySum {
    public static void main(String[] args) throws Exception {
        int[] array = new int[1000];
        Arrays.setAll(array, i -> i + 1); // Rellenamos con números del 1 al 1000

        ExecutorService executor = Executors.newFixedThreadPool(4);

        int chunkSize = array.length / 4;
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            int from = i * chunkSize;
            int to = (i == 3) ? array.length : (i + 1) * chunkSize;

            Callable<Integer> sumTask = () -> {
                int sum = 0;
                for (int j = from; j < to; j++) sum += array[j];
                System.out.println("Suma de " + from + " a " + (to - 1) + " = " + sum);
                return sum;
            };

            futures.add(executor.submit(sumTask));
        }

        int totalSum = 0;
        for (Future<Integer> f : futures) {
            totalSum += f.get(); // Esperamos cada tarea una por una
        }

        System.out.println("Suma total: " + totalSum);

        executor.shutdown();
    }
}

Aquí el array se divide en 4 partes. Para cada parte se crea una tarea (Callable) que calcula la suma. Todas las tareas se envían a ExecutorService, se devuelven Future. Al final reunimos los resultados de todas las tareas y los sumamos.

En casos reales es cómodo usar invokeAll para esperar a que todas las tareas terminen de una vez.

5. Manejo de errores al trabajar con Future

Cuando llamas a future.get(), si la tarea terminó con una excepción, esta se propagará como ExecutionException. Es importante: si en la tarea algo salió mal, te enterarás solo al invocar get().

Ejemplo: manejo de excepciones

Callable<Integer> errorTask = () -> {
    throw new IllegalArgumentException("¡Algo salió mal!");
};

Future<Integer> badFuture = executor.submit(errorTask);

try {
    badFuture.get();
} catch (ExecutionException e) {
    System.out.println("La tarea terminó con error: " + e.getCause());
}
  • Dentro de la tarea se lanza una excepción.
  • Al llamar a get() se «envuelve» en ExecutionException.
  • La causa real se puede obtener con getCause().

6. Detalles útiles

Cómo cancelar una tarea

Future<?> f = executor.submit(() -> {
    while (true) {
        // Trabajo infinito
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("¡Me han pedido que termine!");
            break;
        }
    }
});

Thread.sleep(100); // Esperamos un poco
f.cancel(true); // Intentamos cancelar la tarea
  • cancel(true) intenta interrumpir la tarea si aún no ha finalizado.
  • Dentro de la tarea conviene comprobar Thread.currentThread().isInterrupted() y terminar correctamente.

shutdown vs shutdownNow

shutdown() — parada suave: impide añadir nuevas tareas y permite que las actuales terminen con normalidad. Es la más habitual.

shutdownNow() — parada brusca: intenta interrumpir los hilos activos y devuelve la lista de tareas que no llegaron a arrancar. Úsalo con precaución.

invokeAll e invokeAny

invokeAll(Collection<Callable<T>> tasks) lanza todas las tareas pasadas y espera a que todas terminen. Devuelve una lista de Future.

invokeAny(Collection<Callable<T>> tasks) espera solo a la primera tarea que se complete con éxito, devuelve su resultado y cancela las demás. Útil cuando importa la primera respuesta válida.

7. Errores típicos al trabajar con ExecutorService, Callable y Future

Error n.º 1: no cerrar el ExecutorService. Si olvidas llamar a shutdown(), el programa puede quedarse «colgado» tras finalizar main, porque los hilos del pool esperan nuevas tareas.

Error n.º 2: esperar el resultado justo después de enviar la tarea. Si llamas a get() inmediatamente después de submit(), no obtendrás ventajas de la asincronía: el hilo seguirá esperando. Haz trabajo útil en paralelo y solicita el resultado cuando realmente lo necesites.

Error n.º 3: ignorar las excepciones en las tareas. Si no manejas ExecutionException al invocar get(), puedes pasar por alto errores importantes que ocurrieron dentro de la tarea.

Error n.º 4: usar variables compartidas mutables sin sincronización. Si varias tareas trabajan con los mismos datos, necesitas sincronización o colecciones seguras para hilos.

Error n.º 5: crear demasiados hilos. No conviene hacer un pool con un número de hilos muy superior al de núcleos del procesador: incluso puede ralentizar la ejecución.

Error n.º 6: olvidar cancelar las tareas. Si una tarea ya no es necesaria, cancélala con cancel() para no gastar recursos.

1
Tarea
JAVA 25 SELF, nivel 54, lección 1
Bloqueada
Envío de un paquete urgente con dron 📦
Envío de un paquete urgente con dron 📦
1
Tarea
JAVA 25 SELF, nivel 54, lección 1
Bloqueada
Descifrado de la antigua profecía 🔮
Descifrado de la antigua profecía 🔮
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION