Java
Tutorial Java: ExecutorService
Aprende a usar ExecutorService en Java para gestionar pools de threads, ejecutar tareas concurrentes y controlar el ciclo de vida del executor.
Aprende Java y certifícatePools de threads y tipos de executors
En la programación concurrente de Java, gestionar manualmente la creación y destrucción de threads puede resultar costoso y complejo. Los pools de threads surgen como solución a este problema, permitiendo la reutilización de threads existentes para ejecutar múltiples tareas, evitando así la sobrecarga asociada con la creación constante de nuevos threads.
Concepto de pool de threads
Un pool de threads es una colección de threads de trabajo que esperan para ejecutar tareas. Cuando se envía una tarea al pool, esta se asigna a un thread disponible o se encola hasta que haya uno libre. Este enfoque ofrece varias ventajas:
- Reutilización de recursos: Los threads se crean una vez y se reutilizan para múltiples tareas.
- Control de concurrencia: Limita el número máximo de threads en ejecución simultánea.
- Mejora del rendimiento: Reduce la sobrecarga de crear y destruir threads.
En Java, la interfaz ExecutorService
proporciona métodos para trabajar con pools de threads, y la clase Executors
ofrece métodos de fábrica para crear diferentes tipos de pools.
Tipos de executors en Java
Java proporciona varios tipos de executors predefinidos, cada uno con características específicas para diferentes escenarios:
1. Fixed Thread Pool
Crea un pool con un número fijo de threads. Si se envían más tareas que threads disponibles, las tareas adicionales se encolan hasta que haya threads libres.
// Crear un pool con 5 threads
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// Enviar tareas al pool
for (int i = 0; i < 10; i++) {
final int taskId = i;
fixedPool.execute(() -> {
System.out.println("Tarea " + taskId + " ejecutándose en " + Thread.currentThread().getName());
});
}
Este tipo de pool es ideal para limitar el consumo de recursos en aplicaciones con cargas de trabajo estables.
2. Cached Thread Pool
Crea nuevos threads según sea necesario y reutiliza los existentes cuando están disponibles. Los threads inactivos durante 60 segundos son eliminados.
ExecutorService cachedPool = Executors.newCachedThreadPool();
// El pool creará nuevos threads según sea necesario
for (int i = 0; i < 100; i++) {
final int taskId = i;
cachedPool.execute(() -> {
System.out.println("Tarea " + taskId + " ejecutándose en " + Thread.currentThread().getName());
});
}
Este pool es adecuado para aplicaciones con muchas tareas de corta duración y cargas de trabajo variables.
3. Single Thread Executor
Utiliza un único thread para ejecutar todas las tareas, garantizando que se ejecuten secuencialmente.
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// Todas las tareas se ejecutarán secuencialmente en un único thread
for (int i = 0; i < 5; i++) {
final int taskId = i;
singleThreadExecutor.execute(() -> {
System.out.println("Tarea " + taskId + " ejecutándose en " + Thread.currentThread().getName());
try {
Thread.sleep(500); // Simular trabajo
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
Este executor es útil cuando necesitamos garantizar que las tareas no se ejecuten concurrentemente, por ejemplo, cuando acceden a recursos no thread-safe.
4. Scheduled Thread Pool
Permite programar tareas para que se ejecuten después de un retraso o periódicamente.
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
// Ejecutar una tarea después de 2 segundos
scheduledPool.schedule(() -> {
System.out.println("Tarea retrasada ejecutándose en " + Thread.currentThread().getName());
}, 2, TimeUnit.SECONDS);
// Ejecutar una tarea periódicamente cada 3 segundos, comenzando después de 1 segundo
scheduledPool.scheduleAtFixedRate(() -> {
System.out.println("Tarea periódica ejecutándose en " + Thread.currentThread().getName());
}, 1, 3, TimeUnit.SECONDS);
Este tipo de pool es ideal para tareas programadas o periódicas como actualizaciones de caché, limpieza de recursos, etc.
5. Work Stealing Pool (Java 8+)
Implementa un algoritmo de "robo de trabajo" donde los threads inactivos pueden "robar" tareas de la cola de otros threads ocupados.
ExecutorService workStealingPool = Executors.newWorkStealingPool();
// O especificar el nivel de paralelismo
ExecutorService workStealingPool2 = Executors.newWorkStealingPool(4);
// Enviar tareas con diferentes cargas de trabajo
for (int i = 0; i < 10; i++) {
final int taskId = i;
workStealingPool.submit(() -> {
// Las tareas con ID par tardan más
if (taskId % 2 == 0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Tarea " + taskId + " completada por " + Thread.currentThread().getName());
return taskId;
});
}
Este pool es especialmente eficiente para tareas recursivas (como algoritmos divide y vencerás) y para maximizar la utilización de múltiples núcleos.
Configuración personalizada de pools de threads
Además de los pools predefinidos, podemos crear pools personalizados utilizando ThreadPoolExecutor
directamente:
// Crear un pool personalizado
ThreadPoolExecutor customPool = new ThreadPoolExecutor(
2, // corePoolSize: número mínimo de threads
5, // maximumPoolSize: número máximo de threads
60L, TimeUnit.SECONDS, // keepAliveTime: tiempo que un thread inactivo espera antes de terminar
new LinkedBlockingQueue<>(), // workQueue: cola para tareas pendientes
new ThreadPoolExecutor.CallerRunsPolicy() // rejectionPolicy: qué hacer si la cola está llena
);
// Enviar tareas al pool personalizado
for (int i = 0; i < 10; i++) {
final int taskId = i;
customPool.execute(() -> {
System.out.println("Tarea " + taskId + " ejecutándose en " + Thread.currentThread().getName());
});
}
Esta configuración personalizada nos permite un control preciso sobre el comportamiento del pool, incluyendo:
- Tamaño del núcleo: Número mínimo de threads que se mantienen activos.
- Tamaño máximo: Límite superior de threads que pueden crearse.
- Tiempo de inactividad: Cuánto tiempo espera un thread inactivo antes de terminar.
- Cola de trabajo: Estructura para almacenar tareas pendientes.
- Política de rechazo: Acción a tomar cuando la cola está llena y se alcanza el número máximo de threads.
Consideraciones para elegir el tipo de executor
Al seleccionar un tipo de executor, debemos considerar:
- Naturaleza de las tareas: Duración, frecuencia y dependencias entre tareas.
- Recursos disponibles: Memoria y CPU disponibles en el sistema.
- Requisitos de rendimiento: Latencia vs. throughput.
- Comportamiento bajo carga: Cómo debe responder el sistema cuando hay más tareas que recursos.
// Ejemplo de selección basada en el tipo de aplicación
ExecutorService executor;
if (tasksAreShortLived && workloadIsVariable) {
// Bueno para muchas tareas cortas con carga variable
executor = Executors.newCachedThreadPool();
} else if (resourcesAreLimited) {
// Controla el uso de recursos
executor = Executors.newFixedThreadPool(availableProcessors);
} else if (tasksRequireSequentialExecution) {
// Garantiza ejecución secuencial
executor = Executors.newSingleThreadExecutor();
} else if (tasksAreRecursive || computeIntensive) {
// Maximiza la utilización de CPU para tareas computacionales
executor = Executors.newWorkStealingPool();
} else {
// Configuración personalizada para casos específicos
executor = new ThreadPoolExecutor(/* parámetros personalizados */);
}
Monitorización y ajuste de pools de threads
Para aplicaciones de producción, es importante monitorizar el rendimiento de los pools de threads:
// Obtener estadísticas de un ThreadPoolExecutor
ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
// Enviar algunas tareas...
// Monitorizar el estado del pool
System.out.println("Tamaño del pool: " + pool.getPoolSize());
System.out.println("Tamaño del núcleo: " + pool.getCorePoolSize());
System.out.println("Número de tareas completadas: " + pool.getCompletedTaskCount());
System.out.println("Número de tareas activas: " + pool.getActiveCount());
System.out.println("Tamaño de la cola: " + pool.getQueue().size());
// Ajustar dinámicamente el tamaño del pool
pool.setCorePoolSize(15);
pool.setMaximumPoolSize(20);
La monitorización y el ajuste dinámico permiten optimizar el rendimiento según las condiciones de carga reales.
Submitting tasks: execute() vs. submit()
Una vez configurado un pool de threads mediante ExecutorService
, necesitamos enviar tareas para su ejecución. Java ofrece principalmente dos métodos para este propósito: execute()
y submit()
. Aunque ambos permiten ejecutar tareas de forma asíncrona, presentan diferencias importantes en su comportamiento, manejo de resultados y excepciones.
Método execute()
El método execute()
proviene de la interfaz Executor
(superinterface de ExecutorService
) y representa la forma más básica de enviar una tarea:
void execute(Runnable command);
Este método acepta un objeto Runnable
que encapsula la tarea a ejecutar:
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
System.out.println("Tarea ejecutándose en: " + Thread.currentThread().getName());
// Realizar operaciones...
});
Características principales de execute()
- No devuelve resultado: Al utilizar
Runnable
, no hay forma de obtener un valor de retorno. - Manejo de excepciones limitado: Las excepciones no verificadas que ocurran dentro de la tarea provocarán la terminación del thread, pero no serán propagadas al código que invocó
execute()
. - Comportamiento "fire-and-forget": Una vez enviada la tarea, no hay forma directa de rastrear su estado o cancelarla.
Cuando una tarea lanzada con execute()
genera una excepción, esta se propaga al UncaughtExceptionHandler
del thread (si está configurado) o se imprime en la consola de error:
executor.execute(() -> {
System.out.println("Iniciando tarea con posible error");
// Esta excepción terminará el thread pero no será capturada por el código principal
throw new RuntimeException("Error en la tarea");
});
Método submit()
El método submit()
es específico de ExecutorService
y ofrece capacidades más avanzadas:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> Future<T> submit(Runnable task, T result);
Como podemos ver, submit()
tiene tres sobrecargas que permiten diferentes opciones:
ExecutorService executor = Executors.newFixedThreadPool(2);
// 1. Enviar un Callable que devuelve un resultado
Future<Integer> futureResult = executor.submit(() -> {
// Realizar cálculo
return 42;
});
// 2. Enviar un Runnable (sin resultado)
Future<?> futureVoid = executor.submit(() -> {
System.out.println("Tarea ejecutándose");
});
// 3. Enviar un Runnable con un resultado predefinido
String predefinedResult = "Completado";
Future<String> futureWithResult = executor.submit(() -> {
System.out.println("Procesando...");
}, predefinedResult);
Características principales de submit()
- Devuelve un objeto Future: Permite obtener el resultado de la tarea cuando esté disponible, comprobar si ha terminado o cancelarla.
- Soporte para tareas con valor de retorno: Mediante
Callable<T>
, podemos definir tareas que devuelven un resultado. - Mejor manejo de excepciones: Las excepciones son capturadas y almacenadas en el objeto
Future
, permitiendo manejarlas cuando se llama aFuture.get()
.
Trabajando con Future
El objeto Future
devuelto por submit()
proporciona métodos para gestionar la tarea enviada:
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(() -> {
Thread.sleep(2000); // Simulamos una operación que toma tiempo
return "Resultado de la operación";
});
// Verificar si la tarea ha terminado
System.out.println("¿Completado? " + future.isDone()); // Probablemente false
// Esperar el resultado (bloqueante)
try {
// Espera hasta 3 segundos por el resultado
String result = future.get(3, TimeUnit.SECONDS);
System.out.println("Resultado obtenido: " + result);
} catch (InterruptedException e) {
System.out.println("La operación fue interrumpida");
} catch (ExecutionException e) {
System.out.println("La tarea lanzó una excepción: " + e.getCause());
} catch (TimeoutException e) {
System.out.println("La operación excedió el tiempo máximo");
// Cancelar la tarea si toma demasiado tiempo
future.cancel(true);
}
// Verificar si la tarea fue cancelada
if (future.isCancelled()) {
System.out.println("La tarea fue cancelada");
}
Manejo de excepciones
Una de las diferencias más importantes entre execute()
y submit()
es cómo manejan las excepciones:
ExecutorService executor = Executors.newFixedThreadPool(2);
// Con execute() - la excepción se pierde desde la perspectiva del código principal
executor.execute(() -> {
throw new RuntimeException("Error en execute");
// Esta excepción termina el thread pero no es accesible desde el código principal
});
// Con submit() - la excepción se captura en el Future
Future<?> future = executor.submit(() -> {
throw new RuntimeException("Error en submit");
// Esta excepción se almacena en el Future
});
try {
future.get(); // Aquí se lanzará ExecutionException
} catch (ExecutionException e) {
System.out.println("Excepción capturada: " + e.getCause().getMessage());
// Imprime: "Excepción capturada: Error en submit"
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Patrones comunes de uso
Patrón de ejecución y olvido (Fire-and-forget)
Cuando no necesitamos un resultado ni manejar excepciones:
executor.execute(() -> {
System.out.println("Tarea de notificación ejecutándose");
enviarNotificacion();
// No nos importa el resultado ni las excepciones
});
Patrón de ejecución con resultado (Execute-and-return)
Cuando necesitamos obtener un resultado:
Future<List<Producto>> future = executor.submit(() -> {
return servicioProductos.buscarProductos("smartphone");
});
// Más adelante, cuando necesitemos el resultado:
try {
List<Producto> productos = future.get();
procesarProductos(productos);
} catch (Exception e) {
manejarError(e);
}
Patrón de ejecución con timeout (Execute-with-timeout)
Cuando queremos limitar el tiempo de espera:
Future<DatosCliente> future = executor.submit(() ->
servicioClientes.obtenerDatosCliente(clienteId)
);
try {
// Solo esperamos 500ms por los datos
DatosCliente datos = future.get(500, TimeUnit.MILLISECONDS);
mostrarDatosCliente(datos);
} catch (TimeoutException e) {
future.cancel(true); // Intentamos cancelar la tarea
mostrarDatosParciales(); // Fallback
}
Comparativa: execute() vs. submit()
Característica | execute() | submit() |
---|---|---|
Interfaz | Executor | ExecutorService |
Parámetro | Solo Runnable | Runnable o Callable |
Valor de retorno | void | Future |
Manejo de excepciones | Thread termina, excepción no accesible | Capturada en Future, accesible vía get() |
Cancelación | No soportada | Soportada vía Future.cancel() |
Verificación de estado | No soportada | Soportada vía Future.isDone(), isCancelled() |
Caso de uso típico | Tareas simples sin resultado | Tareas con resultado o que requieren seguimiento |
Consideraciones de rendimiento
El método submit()
ofrece más funcionalidades, pero tiene una pequeña sobrecarga adicional comparado con execute()
:
// Benchmark simplificado
ExecutorService executor = Executors.newFixedThreadPool(4);
long startTime, endTime;
int iterations = 1_000_000;
// Medir execute()
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
executor.execute(() -> {
// Tarea vacía
});
}
endTime = System.nanoTime();
System.out.println("Tiempo execute(): " + (endTime - startTime) / 1_000_000 + " ms");
// Medir submit()
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
executor.submit(() -> {
// Tarea vacía
});
}
endTime = System.nanoTime();
System.out.println("Tiempo submit(): " + (endTime - startTime) / 1_000_000 + " ms");
En la mayoría de los casos, esta diferencia de rendimiento es insignificante comparada con el tiempo de ejecución de las tareas reales, por lo que la elección entre execute()
y submit()
debe basarse principalmente en las necesidades funcionales.
Cuándo usar cada método
Usa execute() cuando:
No necesitas un resultado de la tarea
No necesitas manejar excepciones específicamente
Quieres la máxima eficiencia para tareas simples
Implementas un patrón "fire-and-forget"
Usa submit() cuando:
Necesitas obtener un resultado de la tarea
Necesitas manejar excepciones de forma controlada
Quieres poder cancelar la tarea o verificar su estado
Necesitas establecer timeouts para la ejecución
Shutdown y gestión del ciclo de vida
Cuando trabajamos con ExecutorService
en Java, gestionar adecuadamente su ciclo de vida es tan importante como la creación y ejecución de tareas. Un ExecutorService
consume recursos del sistema mientras está activo, por lo que es fundamental finalizar correctamente estos servicios cuando ya no son necesarios.
Métodos de shutdown
Java proporciona varios métodos para finalizar un ExecutorService
, cada uno con comportamientos diferentes:
shutdown()
El método shutdown()
inicia un apagado ordenado del executor:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Enviamos algunas tareas
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Ejecutando tarea " + taskId);
return taskId;
});
}
// Iniciamos el apagado ordenado
executor.shutdown();
// Verificamos si está en proceso de apagado
System.out.println("¿Está en proceso de apagado? " + executor.isShutdown());
Cuando llamamos a shutdown()
:
- No se aceptan nuevas tareas (lanza
RejectedExecutionException
si intentamos enviar más) - Las tareas previamente enviadas se ejecutan hasta completarse
- El executor no se cierra inmediatamente, sino que espera a que terminen todas las tareas
- Los threads del pool continúan ejecutándose hasta que todas las tareas se completen
Es importante entender que shutdown()
no bloquea el thread que lo llama, simplemente inicia el proceso de apagado.
shutdownNow()
Cuando necesitamos un apagado más agresivo, podemos usar shutdownNow()
:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Enviamos tareas de larga duración
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
try {
System.out.println("Iniciando tarea " + taskId);
Thread.sleep(5000); // Simulamos trabajo de larga duración
System.out.println("Tarea " + taskId + " completada");
return taskId;
} catch (InterruptedException e) {
System.out.println("Tarea " + taskId + " interrumpida");
Thread.currentThread().interrupt();
return null;
}
});
}
// Esperamos un momento para que algunas tareas comiencen
Thread.sleep(1000);
// Apagado inmediato
List<Runnable> pendingTasks = executor.shutdownNow();
System.out.println("Tareas pendientes no iniciadas: " + pendingTasks.size());
Cuando llamamos a shutdownNow()
:
- No se aceptan nuevas tareas
- Intenta detener todas las tareas activas mediante interrupciones
- Devuelve una lista de tareas que estaban en cola pero no llegaron a iniciarse
- No garantiza que las tareas activas se detengan, ya que depende de cómo manejen las interrupciones
Este método es más agresivo y útil en situaciones donde necesitamos liberar recursos rápidamente, incluso a costa de no completar todas las tareas.
Esperar la finalización
En muchos escenarios, necesitamos saber cuándo el executor ha terminado completamente. Para esto, Java proporciona métodos que permiten esperar la finalización:
awaitTermination()
Este método bloquea hasta que todas las tareas hayan completado tras un shutdown, o hasta que se agote el timeout, o el thread sea interrumpido:
ExecutorService executor = Executors.newFixedThreadPool(2);
// Enviamos algunas tareas
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("Tarea " + taskId + " completada");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// Iniciamos el apagado ordenado
executor.shutdown();
try {
// Esperamos hasta 5 segundos para que terminen todas las tareas
boolean completed = executor.awaitTermination(5, TimeUnit.SECONDS);
if (completed) {
System.out.println("Todas las tareas completadas con éxito");
} else {
System.out.println("Tiempo de espera agotado, algunas tareas no completadas");
}
} catch (InterruptedException e) {
System.out.println("Espera interrumpida");
Thread.currentThread().interrupt();
}
El método awaitTermination()
es bloqueante y devuelve:
true
si el executor terminó completamente dentro del tiempo especificadofalse
si el timeout se agotó antes de que terminara
Patrón de apagado seguro
Un patrón común para un apagado seguro combina shutdown()
con awaitTermination()
:
void apagarExecutorDeFormaSegura(ExecutorService executor) {
executor.shutdown(); // Rechazar nuevas tareas
try {
// Esperar un tiempo razonable para la finalización normal
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// Si las tareas no terminan en 60 segundos, forzar apagado
executor.shutdownNow();
// Esperar que las tareas respondan a la interrupción
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("El executor no terminó");
}
}
} catch (InterruptedException e) {
// Preservar el estado de interrupción
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Este patrón intenta primero un apagado ordenado y, si las tareas tardan demasiado, recurre a un apagado forzado.
isShutdown() vs isTerminated()
Java proporciona dos métodos para verificar el estado del executor:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
executor.shutdown();
System.out.println("isShutdown: " + executor.isShutdown()); // true
System.out.println("isTerminated: " + executor.isTerminated()); // false
// Esperamos a que termine
Thread.sleep(3000);
System.out.println("isShutdown: " + executor.isShutdown()); // true
System.out.println("isTerminated: " + executor.isTerminated()); // true
- isShutdown(): Devuelve
true
si el executor ha iniciado el proceso de apagado (se ha llamado ashutdown()
oshutdownNow()
). - isTerminated(): Devuelve
true
solo cuando todas las tareas han completado y el executor ha terminado completamente.
Gestión de recursos con try-with-resources
Desde Java 7, podemos usar el patrón try-with-resources para gestionar automáticamente el ciclo de vida de un ExecutorService
, implementando la interfaz AutoCloseable
:
class ManagedExecutor implements AutoCloseable {
private final ExecutorService executor;
public ManagedExecutor(int threads) {
this.executor = Executors.newFixedThreadPool(threads);
}
public ExecutorService getExecutor() {
return executor;
}
@Override
public void close() {
executor.shutdown();
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
// Uso con try-with-resources
try (ManagedExecutor managed = new ManagedExecutor(4)) {
ExecutorService executor = managed.getExecutor();
// Usar el executor...
executor.submit(() -> System.out.println("Tarea ejecutada"));
// Al salir del bloque try, se llama automáticamente a close()
}
Este enfoque garantiza que el executor se cierre correctamente incluso si ocurren excepciones.
Manejo de tareas pendientes durante el shutdown
Cuando iniciamos un apagado, es posible que haya tareas pendientes que no lleguen a ejecutarse. Podemos recuperar estas tareas con shutdownNow()
y decidir qué hacer con ellas:
ExecutorService executor = Executors.newSingleThreadExecutor();
// Enviamos más tareas de las que pueden procesarse inmediatamente
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
if (taskId == 0) {
try {
// La primera tarea tarda mucho
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return "Resultado " + taskId;
});
}
// Esperamos un poco para que la primera tarea comience
Thread.sleep(100);
// Apagado inmediato
List<Runnable> pendingTasks = executor.shutdownNow();
System.out.println("Tareas no ejecutadas: " + pendingTasks.size());
// Podemos procesar las tareas pendientes de otra manera
for (Runnable task : pendingTasks) {
if (task instanceof Future) {
System.out.println("Tarea pendiente recuperada");
// Podríamos ejecutar estas tareas en otro executor o guardarlas
}
}
Hooks de apagado en la JVM
Para aplicaciones de larga duración, es recomendable registrar hooks de apagado que cierren correctamente los executors cuando la JVM se cierre:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Registrar un hook de apagado
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Apagando executor desde shutdown hook");
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("Forzando apagado del executor");
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}));
// Resto del código de la aplicación...
Este hook garantiza que los recursos se liberen adecuadamente incluso si la aplicación termina abruptamente (por ejemplo, con Ctrl+C).
Prácticas recomendadas
Para una gestión efectiva del ciclo de vida de los executors:
- Cierra siempre los executors: Un executor no cerrado puede impedir que la JVM termine.
- Usa shutdown() para apagados normales: Permite que las tareas en curso terminen.
- Usa shutdownNow() para emergencias: Cuando necesitas liberar recursos inmediatamente.
- Implementa timeouts razonables: No esperes indefinidamente a que terminen las tareas.
- Maneja las interrupciones: Asegúrate de que tus tareas respondan adecuadamente a las interrupciones.
- Considera usar try-with-resources: Para garantizar el cierre automático.
- Registra shutdown hooks: Para aplicaciones de larga duración.
// Ejemplo de buena práctica en un método
public void procesarDatos(List<Dato> datos) {
ExecutorService executor = Executors.newFixedThreadPool(
Math.min(datos.size(), Runtime.getRuntime().availableProcessors())
);
try {
// Enviar tareas al executor
List<Future<Resultado>> futures = datos.stream()
.map(dato -> executor.submit(() -> procesarDato(dato)))
.collect(Collectors.toList());
// Recoger resultados
for (Future<Resultado> future : futures) {
try {
Resultado resultado = future.get(30, TimeUnit.SECONDS);
// Procesar resultado...
} catch (Exception e) {
// Manejar excepciones...
}
}
} finally {
// Garantizar que el executor se cierre
executor.shutdown();
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
La gestión adecuada del ciclo de vida de los executors es esencial para evitar fugas de recursos y garantizar que las aplicaciones se comporten correctamente tanto durante la operación normal como durante el apagado.
Ejercicios de esta lección ExecutorService
Evalúa tus conocimientos de esta lección ExecutorService con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Streams: match
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
Todas las lecciones de Java
Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Instalación De Java
Introducción Y Entorno
Configuración De Entorno Java
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Recursión
Sintaxis
Arrays Y Matrices
Sintaxis
Excepciones
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Sobrecarga De Métodos
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
La Clase Scanner
Programación Orientada A Objetos
Métodos De La Clase String
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Records
Programación Orientada A Objetos
Pattern Matching
Programación Orientada A Objetos
Inferencia De Tipos Con Var
Programación Orientada A Objetos
Enumeraciones Enums
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Clases Sealed
Programación Orientada A Objetos
Listas
Framework Collections
Conjuntos
Framework Collections
Mapas
Framework Collections
Funciones Lambda
Programación Funcional
Interfaz Funcional Consumer
Programación Funcional
Interfaz Funcional Predicate
Programación Funcional
Interfaz Funcional Supplier
Programación Funcional
Interfaz Funcional Function
Programación Funcional
Métodos Referenciados
Programación Funcional
Creación De Streams
Programación Funcional
Operaciones Intermedias Con Streams: Map()
Programación Funcional
Operaciones Intermedias Con Streams: Filter()
Programación Funcional
Operaciones Intermedias Con Streams: Distinct()
Programación Funcional
Operaciones Finales Con Streams: Collect()
Programación Funcional
Operaciones Finales Con Streams: Min Max
Programación Funcional
Operaciones Intermedias Con Streams: Flatmap()
Programación Funcional
Operaciones Intermedias Con Streams: Sorted()
Programación Funcional
Operaciones Finales Con Streams: Reduce()
Programación Funcional
Operaciones Finales Con Streams: Foreach()
Programación Funcional
Operaciones Finales Con Streams: Count()
Programación Funcional
Operaciones Finales Con Streams: Match
Programación Funcional
Api Optional
Programación Funcional
Transformación
Programación Funcional
Reducción Y Acumulación
Programación Funcional
Mapeo
Programación Funcional
Streams Paralelos
Programación Funcional
Agrupación Y Partición
Programación Funcional
Filtrado Y Búsqueda
Programación Funcional
Api Java.nio 2
Entrada Y Salida Io
Fundamentos De Io
Entrada Y Salida Io
Leer Y Escribir Archivos
Entrada Y Salida Io
Httpclient Moderno
Entrada Y Salida Io
Clases De Nio2
Entrada Y Salida Io
Api Java.time
Api Java.time
Localtime
Api Java.time
Localdatetime
Api Java.time
Localdate
Api Java.time
Executorservice
Concurrencia
Virtual Threads (Project Loom)
Concurrencia
Future Y Completablefuture
Concurrencia
Spring Framework
Frameworks Para Java
Micronaut
Frameworks Para Java
Maven
Frameworks Para Java
Gradle
Frameworks Para Java
Lombok Para Java
Frameworks Para Java
Quarkus
Frameworks Para Java
Ecosistema Jakarta Ee De Java
Frameworks Para Java
Introducción A Junit 5
Testing
Certificados de superación de Java
Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el concepto y ventajas de los pools de threads en Java.
- Identificar y utilizar los diferentes tipos de executors predefinidos y personalizados.
- Diferenciar entre los métodos execute() y submit() para enviar tareas y gestionar resultados y excepciones.
- Aprender a gestionar el ciclo de vida de un ExecutorService mediante shutdown(), shutdownNow() y awaitTermination().
- Aplicar buenas prácticas para la monitorización, ajuste y cierre seguro de pools de threads en aplicaciones concurrentes.