Executor Service

Executor Service

Cuando se necesitan crear multiples threads, no es óptimo crearlos manualmente usando la clase thread, dado que estos threads son los threads que se crean a nivel de SO, y supone un gran coste el crear nuevos threads (cada hilo a nivel de sistema operativo tiene un costo de memoria y administración). Por lo que existen algunas clases que nos ayudan a gestionar y crear una pool de threads. Estas son las clases que estan bajo la interfaz ExecutorService.

Una de las particularidades de esta clase es que crea un pool de thread y cuando algún thread termina de realizar su operación, este thread vuelve al pool, permitiendo reutilizarlo para realizar otras operaciones:

Implementación Descripción
Implementaciones principales (ExecutorService)
ThreadPoolExecutor Implementación más flexible de un pool de hilos. Permite definir el tamaño del pool, estrategias de cola y manejo de tareas rechazadas. Se usa para personalizar la administración de hilos.
ScheduledThreadPoolExecutor Extiende ThreadPoolExecutor. Permite ejecutar tareas con retraso o de forma periódica. Útil para tareas programadas como cron jobs.
ForkJoinPool Diseñado para dividir tareas grandes en subtareas más pequeñas (divide and conquer). Se usa para procesamiento paralelo (parallelStream(), algoritmos recursivos).
Métodos de fábrica (Executors) (No son clases, pero crean instancias de ThreadPoolExecutor con diferentes configuraciones)
Executors.newFixedThreadPool(n) Crea un ThreadPoolExecutor con un número fijo de n hilos. Útil para tareas con carga de trabajo constante.
Executors.newCachedThreadPool() Crea un ThreadPoolExecutor con un número variable de hilos. Crea nuevos hilos si es necesario y reutiliza los inactivos. Ideal para tareas cortas y dinámicas.
Executors.newSingleThreadExecutor() Crea un ThreadPoolExecutor con un solo hilo. Garantiza la ejecución secuencial de tareas. Útil cuando se necesita orden y exclusividad en la ejecución.
Executors.newScheduledThreadPool(n) Crea un ScheduledThreadPoolExecutor con n hilos, útil para programar tareas con retraso o ejecución periódica.
Executors.newSingleThreadScheduledExecutor() Crea un ScheduledThreadPoolExecutor con un solo hilo para ejecutar tareas programadas en orden secuencial.
Executors.newWorkStealingPool() (Java 8+) Crea un ForkJoinPool que usa el algoritmo de “robo de trabajo” (work-stealing). Útil para mejorar la utilización de CPU en tareas paralelas.

image.png

  • Main Thread (Hilo principal)
    • El hilo principal ejecuta un bucle for(int i = 0; i < n; i++), donde envía tareas al ThreadPool usando executor.execute(new Task()). Esto significa que el hilo principal no ejecuta las tareas directamente, sino que las delega al pool de hilos.
  • Thread Pool (Pool de Hilos)
    • Es un grupo de hilos precreados que pueden reutilizarse en lugar de crear y destruir hilos constantemente. Contiene una Blocking Queue (cola bloqueante) donde se almacenan las tareas que aún no han sido asignadas a un hilo.
  • Funcionamiento del Thread Pool
    • Cuando una tarea es enviada con executor.execute(new Task()), esta se coloca en la Blocking Queue si no hay un hilo disponible. Un hilo del pool (Thread) toma una tarea de la cola y la ejecuta. Si hay más tareas pendientes, los hilos siguen sacando tareas de la cola y ejecutándolas.
  • Múltiples hilos ejecutando tareas
    • Varios hilos trabajan en paralelo, ejecutando tareas de la cola de manera eficiente. Esto evita el overhead de crear nuevos hilos repetidamente y mejora el rendimiento.

SingleThreadExecutor

En el SingleThreadExecutor dentro del pool de threads solamente hay un thread, y las tareas se van apilando en la queue. Como solamente existe un thread dentro del pool, esta garantizado que las tareas serán ejecutadas secuencialmente

image.png

CachedThreadExecutor

En el caso de CachedThreadExecutor no tenemos un número fijo de threads dentro del pool. Otra particularidad especial de este pool es que no se guardan todas las tareas que ejecutamos en una blocking queue, la queue en este caso es de un tipo especial, y es llamada SynchronousQueue.

Esta cola no almacena tareas, sino que simplemente transfiere directamente una tarea a un thread trabajador. Si no hay un thread disponible para tomar la tarea de inmediato, se crea un nuevo thread. Si ya existe uno, le pasa la tarea a este thread.

Si no hay thread disponible, el executor creará un nuevo thread y lo añadira al thread pool, y este nuevo thread empezará a ejecutar la tarea que fue enviada.

Imaginemos un escenario donde 10 threads están ejecutando sus tareas, y enviamos una nueva tarea al CachedThreadPool, dado que no tenemos un thread disponible para esta tarea, el executor creará un nuevo thread (thread-11) para ejecutar esta nueva tarea. CachedThreadPool es capaz de crear miles y miles de threads.

Si tenemos varios threads y estos no reciben ningún trabajo en los últimos 60 segundos, la implementación empezará a terminar algunos threads (kill thread), por lo que este sistema es autoescalable por naturaleza. Manteniendo los necesarios para ejecutar las tareas que haya en ese momento. Este comportamiento evita el desperdicio de recursos en períodos de baja carga, haciéndolo autoajustable según la demanda.

image.png

ScheduledServiceExecutor

La diferencia entre las anteriores implementaciones, es que se dispone de una delay queue por lo que las tareas que tengan mayor delay, serán las que serán pasadas más tarde a los diferentes threads.

Captura de pantalla 2025-02-24 a las 21.12.33.png