Thread Pools en Java 1.5 / 1.6

Algo nuevo que apareció en Java 5 y no he visto que se le haga mucha promoción, cuando es algo muy útil, son los thread pools. El paquete java.util.concurrent define varias clases para usarse en ambientes de alta concurrencia (es decir, muchos threads realizando tareas simultáneamente, incluso teniendo acceso a los mismos recursos).
En este artículo describo las ventajas de los distintos tipos de thread pools, así como la manera de utilizarlos.

Antes
Un escenario común en algunas aplicaciones de alta concurrencia es por ejemplo estar recibiendo mensajes o peticiones de un sistema externo, o de usuarios del sistema, etc. Dichas peticiones se deben atender tan pronto como sea posible. Las opciones son:

  1. Procesar cada tarea en un thread nuevo
  2. Procesar todas las tareas de manera secuencial en un thread

La primera opción no es muy escalable porque bajo carga fuerte, se pueden llegar a crear tantos threads que el consumo de memoria sea altísimo y la sincronización haga que las tareas se procesen de manera muy lenta. La segunda opción también puede tener como resultado que las tareas se procesen de manera muy lenta bajo una carga fuerte, pues cada tarea nueva será procesada hasta que se terminen todas las tareas pendientes que se encolaron antes.

Ahora
La solución que ofrecen los thread pools es una combinación de ambas opciones. Un esquema en donde se puede crear un número determinado de threads, los cuales van procesando las tareas que se toman de una cola. De esta manera, se están procesando varias tareas simultáneamente, pero bajo un esquema de carga fuerte no se van a crear demasiados threads sino que se tiene un número límite y las tareas se encolan (pero se procesan más rápido que si fueran completamente secuenciales).

La manera más sencilla de crear y utilizar un thread pool es por medio de la clase Executors, la cual ofrece métodos para crear distintos tipos de thread pools. Para explicar el funcionamiento de cada uno, vamos a suponer un ejemplo en el que el sistema está primero inactivo, luego llegan 10 tareas simultáneamente, pasan 10 segundos, llegan 5 tareas simultáneamente, pasan 5 minutos y llegan 20 tareas simultáneamente. Cada tarea toma entre 5 y 15 segundos en procesarse.

  crea un pool que va a ir creando threads conforme se vayan necesitando, pero puede reutilizar threads inactivos. Bajo este esquema, cada una de las 10 tareas que llegan primero, se ejecutan cada una en un thread nuevo. Cuando llegan las siguientes 5 tareas, se buscan primero los threads que ya hayan terminado de procesar su tarea; si hay threads libres, se ponen las tareas a procesarse dentro de dichos threads, y las tareas faltantes se procesan en threads nuevos. Por ejemplo, si ya hay 3 threads libres, se procesan ahi 3 de las tareas nuevas, y se crean 2 threads nuevos para procesar las 2 tareas faltantes. En este momento se tienen 12 threads funcionando, que al final habrán procesado 15 tareas. Los threads que llevan mucho tiempo inactivos son terminados automáticamente por el pool, de manera que el número de threads para procesar tareas va bajando cuando el sistema está inactivo e incluso se puede quedar vacío, como al principio. Cuando lleguen nuevas tareas, se crearán los threads necesarios para procesarlas. Es decir, cuando lleguen 20 tareas 5 minutos después, seguramente van a correr en 20 threads nuevos.

  crea un pool con el número de threads indicado; dichos threads siempre estarán listos para procesar tareas. El pool maneja también una cola de tareas; cada thread toma una tarea de la cola y la procesa, y al terminar toma otra tarea de la cola para procesarla, etc en un ciclo. Bajo este esquema, las primeras 10 tareas que llegan se ponen en la cola y los threads desocupados toman cada uno una tarea y la procesan. Cuando llegan las siguientes 5 tareas, se ponen en la cola y los primeros threads que se desocupen irán tomando las tareas de la cola para procesarlas. Cuando lleguen 20 tareas 5 minutos después, se ponen en la cola y los threads van a tomar las primeras 10 (dado que están todos desocupados); cada thread cuando termine de procesar su tarea, tomará otra tarea de la cola.

  crea un pool de un solo thread, con una cola en donde se ponen las tareas a procesar; el thread toma una tarea de la cola, la procesa y toma la siguiente, en un ciclo. Similar a la segunda opción sin thread pools; llegan 10 tareas, y se van a procesar de manera secuencial, las siguientes 5 se van a encolar y se procesarán cuando se terminen las primeras 10, y las ultimas 20 se procesarán una tras otra. La ventaja que ofrece este esquema es que cuando se necesita realizar algo asi, ya no se tiene que codificar desde cero, sino que simplemente se puede usar este pool de un solo thread, el cual además tiene la ventaja de que si ocurre una excepción durante la ejecución de una tarea, no se detiene la ejecución de las siguientes.

  crea un pool que va a ejecutar tareas programadas cada cierto tiempo, ya sea una sola vez o de manera repetitiva. Es parecido a un timer, pero con la diferencia de que puede tener varios threads que irán realizando las tareas programadas conforme se desocupen. También hay una versión de un solo thread.

Tareas
Bueno pero y ¿qué son las tareas? Simplemente son Runnables. Cualquier objeto que implemente la interfaz Runnable se puede correr dentro de un thread pool. Simple, ¿no?
En caso que se necesite mayor control sobre las tareas encoladas y/o en ejecución dentro del thread pool, se pueden usar FutureTasks, una nueva clase del mismo paquete, que envuelve un Runnable y guarda un objeto que se devuelve como resultado de la ejecución. Para usarse, simplemente se crea un FutureTask con el Runnable que representa la tarea, así como el objeto que representa el resultado, y se ejecuta dentro del thread pool. El FutureTask tiene metodos como   para que se cancele la tarea (cuando le toca su turno en el thread pool, simplemente ya no hace nada),   que devuelve true si ya fue procesada la tarea, y por supuesto  , que devuelve el objeto de resultado pero solamente cuando la tarea termina de procesarse; si se invoca el método antes de que se ejecute o termine de ejecutar, se bloquea el thread donde se llamó el método.

Conclusión
Para aplicaciones donde se deben realizar varias tareas de manera simultánea, los thread pools son una solución poderosa y sencilla de usar. Este es un concepto que existe desde hace mucho pero se tenían que usar librerías externas o hacer una implementación propia; ahora que vienen como parte del JDK es bastante más sencillo usar esta nueva funcionalidad, sobre todo porque hay varias opciones distintas, ya que en distintas situaciones se puede necesitar un distinto tipo de thread pool.

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Agradecimiento

Hola, gracias por el artículo. Me parecio muy bien explicado y conciso.

Imagen de daynatem

Aportando

Bueno atendiendo a cierto tweet jajaja
pues dejo un link para que puedan saber mas del tema que me pareció super interesante, de hecho hace tiempo hice algo pero no sabia que existía estas clases apesar de que busque un poco al respecto.

Ya saben esta no falla.

  • Y un libro al respecto
  • Imagen de Joxebus

    Excelente aporte.

    Me ha parecido muy bien explicado y muy útil el conocer que existe esta clase, ha decir verdad aún después de terminar de leer el artículo aún me quedo un poco de duda sobre como utilizarlo, pero creo que solo para complementar tu aporte encontré una página donde viene un ejemplo de cómo se utilizan ya en la práctica.

    Aquí el link:

  • El ejemplo: