Saltar al contenido principal

Executors y tareas

Crear un Thread ayuda a entender el mecanismo, pero vincula cada tarea a un hilo concreto. Cuando hay muchas tareas interesa administrar trabajo y limitar los hilos disponibles.

De hilos a tareas​

Executor acepta tareas. ExecutorService añade envío con resultado, cierre y espera. Un pool reutiliza un conjunto limitado de hilos:

ExecutorService executor = Executors.newFixedThreadPool(2);
try {
for (int i = 1; i <= 4; i++) {
int id = i;
executor.execute(() -> System.out.printf(
"Tarea %d en %s%n", id, Thread.currentThread().getName()));
}
} finally {
executor.shutdown();
}

El pool fijo limita cuántas tareas pueden ejecutarse simultáneamente. No garantiza su orden. newSingleThreadExecutor() usa un único trabajador y ejecuta sus tareas secuencialmente, aunque no debemos depender de nombres internos del hilo.

Más hilos no significa automáticamente más rendimiento. Las cargas de CPU e I/O tienen necesidades distintas y el tamaño adecuado depende del trabajo y del entorno; no hay una fórmula universal.

execute y submit​

  • execute(Runnable) no devuelve Future.
  • submit(...) devuelve un Future que representa el resultado, fallo o cancelación.

Una excepción de una tarea enviada con submit puede quedar representada en el Future; no hay que asumir que imprimirá automáticamente un stack trace.

Callable y Future​

ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Integer> futuro = executor.submit(() -> {
System.out.println("Calculando en " + Thread.currentThread().getName());
return 21 * 2;
});

System.out.println("Resultado: " + futuro.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.err.println("La tarea falló: " + e.getCause().getMessage());
} finally {
executor.shutdown();
}

get() puede bloquear hasta que haya resultado. El orden en que recuperamos varios futures no demuestra el orden real en que terminaron sus tareas.

try {
String resultado = futuro.get(500, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
System.out.println("No terminó dentro del límite");
} catch (CancellationException e) {
System.out.println("El future estaba cancelado");
} catch (ExecutionException e) {
System.err.println("La tarea falló: " + e.getCause().getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

El timeout solo limita la espera del coordinador: no termina por sí mismo la tarea. La política de cancelación se estudiará en JV5-05.

Fallos de tareas​

Future<Integer> futuro = executor.submit(() -> {
throw new IllegalStateException("dato inválido");
});

try {
futuro.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.out.println(e.getCause().getClass().getSimpleName());
}

ExecutionException envuelve el fallo original, disponible con getCause(). Está garantizado que get() refleja el resultado de esa tarea; no está garantizado cuándo terminarán otras tareas del pool.

Cierre robusto en Java 17​

En Java 17 ExecutorService no se usa con try-with-resources. Se cierra explícitamente:

ExecutorService executor = Executors.newFixedThreadPool(2);
try {
// enviar y recoger tareas
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(2, TimeUnit.SECONDS)) {
executor.shutdownNow();
if (!executor.awaitTermination(2, TimeUnit.SECONDS)) {
System.err.println("El executor no terminó");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}

shutdown() deja de aceptar tareas y permite terminar las ya enviadas. shutdownNow() solicita interrupción y devuelve tareas que no comenzaron; no garantiza terminación instantánea.

Experimento​

Envía cuatro tareas a un pool de dos hilos. Antes de ejecutar, pregunta qué propiedades están permitidas. Repite cinco veces y anota evidencias.

  • Garantizado: cada tarea enviada se ejecuta como máximo una vez si no es rechazada; tras una terminación correcta del executor no quedan tareas activas.
  • Posible: orden y trabajadores distintos entre ejecuciones.
  • Observado: describe solo la salida concreta de tu máquina.

Práctica​

Convierte dos cálculos manuales en tareas con resultado y suma sus valores.

Solución​

ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<Integer> a = executor.submit(() -> 20 + 22);
Future<Integer> b = executor.submit(() -> 6 * 7);
System.out.println(a.get() + b.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.err.println(e.getCause().getMessage());
} finally {
executor.shutdown();
try {
if (!executor.awaitTermination(2, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}

El resultado es 84. No podemos deducir qué cálculo terminó primero.

Errores habituales​

  • Crear un hilo por tarea sin límite.
  • Olvidar shutdown().
  • Creer que get() no bloquea.
  • Ignorar la causa de ExecutionException.
  • Suponer que submit imprime cualquier excepción.
  • Usar try-with-resources con ExecutorService bajo Java 17.

En JV5-04 estudiaremos qué ocurre cuando varias tareas acceden al mismo estado mutable.