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 devuelveFuture.submit(...)devuelve unFutureque 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
submitimprime cualquier excepción. - Usar try-with-resources con
ExecutorServicebajo Java 17.
En JV5-04 estudiaremos qué ocurre cuando varias tareas acceden al mismo estado mutable.