Skip to content

2 ForkJoinPool

Intro

La clase ForkJoinPool es un tipo de ejecutor, que al igual que ThreadPoolExecutor, hereda de AbstractExecutorService, y por tanto implementa las interfaces ExecutorService y Executor.

Al heredar de AbstractExecutorService, implementa todos los métodos característicos de la interfaz ExecutorService que hemos estudiado antes, tanto para el envío de objetos Runnable y Callable, como execute(runnable), submit(callable), invokeAll(callableCollection) e invokeAny(callableCollection), y los métodos relacionados con la terminación explícita del ejecutor, como shutdown(), shutdownNow() o awaitTermination().

En los métodos anteriores, un ejecutor ForkJoinPool se comporta de forma similar a un ThreadPoolExecutor.

Sin embargo, la clase ForkJoinPool proporciona una característica adicional que lo hace muy especial: implementa el algoritmo de robo de trabajo. Esta características sólo está disponible en un tipo especial de tareas diseñadas para trabajar con este tipo de ejecutor, representadas por la clase ForkJoinTask.

Podemos crear un ejecutor ForkJoinPool a través del constructor de dicha clase ForkJoinPool(), que hace que el ejecutor se cree con un threadpool de un tamaño igual al número de núcleos de procesamiento disponibles en el sistema. El constructor está sobrecargado ForkJoinPool(parallelism) para recibir el número de hilos que debe tener el threadpool del ejecutor, lo que nos puede ser útil para indicar un número inferior al número real de núcleos de procesamiento disponibles.

Aunque es posible crear un ejecutor ForkJoinPool a través de alguno de sus constructores, la opción recomendada es usar el método estático ForkJoinPool.commonPool(), que retorna un ForkJoinPool ya existente, creado por el sistema, común a todas la aplicaciones, y que que usa tantos hilos como núcleos de procesamiento haya disponibles en el sistema. El objetivo de usar este ForkJoinPool común es el de reducir el uso de recursos. De hecho, y como veremos más adelante, los objetos ForkJoinTask lo usan si no se indica explícitamente el ForkJoinPool que debe usarse.

Diagrama de clases de ForkJoinPool

Figura 2 - Diagrama de clases de ForkJoinPool

La clase ForkJoinPool proporciona nuevos métodos para poder enviar objetos ForkJoinTask al ejecutor. Estos métodos serán usados por clientes externos al propio ForkJoinPool (non ForkJoinTask clients). Debemos tener en cuenta que las propias tareas ForkJoinTask que se estén ejecutando en el ejecutor pueden a su vez enviar subtareas al ejecutor, pero no lo hacen usando estos métodos, sino otros métodos que estudiaremos dentro de poco:

  • execute(ForkJoinTask<?> tarea): Envía al ejecutor una tarea ForkJoinTask que no retorna ningún valor. El hilo desde el que se realiza el envío NO es bloqueado, por lo que puede continuar con su ejecución. La llamada a este método no retorna nada.
  • submit(ForkJoinTask<T> tarea): Envía al ejecutor una tarea ForkJoinTask que retorna un valor de tipo T. El hilo desde el que se realiza el envío NO es bloqueado, por lo que puede continuar con su ejecución. La llamada a este método retorna un ForkJoinTask<T>, que extiende de Future<T>, en el que se almacenará el valor del tipo T retornado por la tarea cuando ésta termine de ejecutarse.
  • invoke(ForkJoinTask<T> tarea): Envía al ejecutor una tarea ForkJoinTask que retorna un valor de tipo T. El hilo desde el que se realiza el envío es bloqueado hasta que termine de ejecutarse la tarea enviada. La llamada a este método retorna el valor del tipo T retornado por la tarea.

El ForkJoinPool dispone de una cola compartida (shared queue) en la que se encolan las ForkJoinTasks enviadas por clientes externos. Por su parte, cada hilo del threadpool del ejecutor posee su propia cola de trabajo (work queue) de la que extraen las tareas para ejecutarlas, que corresponde a una cola de doble extremo (dequeue, double-ended queue), es decir, una cola en la que se pueden realizar inserciones y extracciones por ambos extremos.

Cuando se recibe una ForkJoinTask en la cola compartida y algún hilo con su cola de trabajo vacía, la tarea se extrae de la cola compartida y se lleva a la cola de trabajo de dicho hilo.

Cola compartida del ForkJoin pool

Figura 3 - Cola compartida del ForkJoin pool

La clase ForkJoinPool proporciona una serie de métodos informativos para obtener información sobre su ejecución:

  • getPoolSize(): Retorna el número de hilos trabajadores usados por el ejecutor.
  • getParallelism(): Retorna el nivel deseado de paralelismo establecido para el ejecutor.
  • getActiveThreadCount(): Retorna el número de hilos del ejecutor que están ejecutando tareas en este momento.
  • getRunningThreadCount(): Retorna el número de hilos que no están bloqueados en ningún mecanismo de sincronización.
  • getQueuedSubmissionCount(): Retorna el número de tareas que han sido enviadas al ejecutor y que aún no han comenzado su ejecución.
  • getQueuedTaskCount(): Retorna el número de tareas que han sido enviadas al ejecutor y que ya han comenzado su ejecución.
  • hasQueuedSubmissions(): Retorna true si el ejecutor tiene tareas pendientes de comenzar a ejecutar.
  • getStealCount(): Retorna el número de veces que se ha producido robo de trabajo (lo estudiaremos dentro de poco).
  • isTerminated(): Retorna true si el ejecutor ya ha finalizado su ejecución.