Multitarea e Hilos en Java con ejemplos II (Runnable & Executors)
El proyecto de este post lo puedes descargar pulsando AQUI.
NOTA: A lo largo de esta entrada se utiliza la palabra «thread» (con t minúscula) refiriéndonos al concepto de hilo o procesamiento independiente y la palabra «Thread» (con T mayúscula) refiriéndonos a la clase Thread de Java.
En la entrada «Multitarea e Hilos en Java con ejemplos (Thread & Runnable)» vimos las diferentes maneras de trabajar con Threads en Java, bien sea heredando de la clase Thread o implementado la clase Runnable. También vimos las ventajas del uso de la multitarea y como nuestros programas pueden ejecutar procesos de forma paralela siempre y cuando estos sean independientes unos de otros.
Como ejemplo de la entrada anterior se simuló un proceso de cobro de productos en un supermercado en el que dos clientes van con un carro lleno de productos y una cajera (un thread) o dos cajeras (dos threads) pasan los productos por el escaner para cobrarles la compra. Los productos que llevan los clientes fueron representados por un array de enteros (int) en el que cada entero representaba los segundos que la cajera tardaba en procesar el producto. El ejemplo propuesto tenia una pequeña pega y es que poníamos tantas cajeras (o threads) como clientes había y por tanto pusimos dos cajeras para procesar los productos de dos clientes, pero ¿Que pasaría si en vez de dos clientes (2 procesos) tuviésemos 100 clientes?, ¿Tendríamos que poner 100 cajeras (crear 100 threads)?. Otra cosa que se explicó es que los threads son ejecuciones en paralelo y estos están muy ligados al número de núcleos (o unidades de proceso) de nuestro hardware (u ordenador) y por tanto debemos de tener en cuenta los recursos hardware a la hora de hacer nuestras implementaciones con threads.
En resumen, lo que queremos mostrar en esta entrada es como gestionar la ejecución de threads. Suponer de nuevo el ejemplo del supermercado, pero en este caso en vez de tener 2 clientes, tendremos 8 clientes (8 procesos) y dos cajeras (2 threads). Evidentemente los clientes tienen que ser procesados de uno en uno, y por tanto se deben de poner en cola y ser procesados de uno en uno. Para ello podríamos implementarnos un sistema de «colas o pilas» para la gestión de threads, pero Java ya nos proporciona la interface «ExecutorService» y la clase «Executors» (que implementa la interface ExecutorService) para la gestión de los threads. La clase «Executors» es la encargada de gestionar la ejecución de los threads en función del número de threads que utilicemos. Para hacer una similitud con el ejemplo que vamos a poner, la clase «Executors» va a ser la encargada de organizar la cola de los Clientes y mandar a los Clientes a la Cajera correspondiente cuando esta haya terminado de procesar la compra del Cliente anterior. Para ello veamos el siguiente ejemplo:
Definimos la clase Cliente con un atributo «nombre» y un array de enteros, en el que cada entero representa un producto del carro y su valor el tiempo que tarda la cajera en escanear el producto; es decir, que si tenemos un array con [1,3,5] significará que el cliente ha comprado 3 productos y que la cajera tardara en procesar el producto 1 ‘1 segundo’, el producto 2 ‘3 segundos’ y el producto 3 en ‘5 segundos’, con lo cual tardara en cobrar al cliente toda su compra ‘9 segundos’:
public class Cliente { private String nombre; private int[] carroCompra; // Constructor, getter y setter }
Para utilizar la clase Executors, debemos de implementar la clase «Runnable» y no sobreescribir (o heredar) de la clase «Thread», ya que la clase Executors solo gestiona clases con interface Runnable; por tanto, la clase Cajera la implementamos de la siguiente manera:
public class CajeraRunnable implements Runnable { private Cliente cliente; private long initialTime; public CajeraRunnable(Cliente cliente, long initialTime) { this.cliente = cliente; this.initialTime = initialTime; } @Override public void run() { System.out.println("La cajera " + Thread.currentThread().getName() + " COMIENZA A PROCESAR LA COMPRA DEL CLIENTE " + this.cliente.getNombre() + " EN EL TIEMPO: " + (System.currentTimeMillis() - this.initialTime) / 1000 + "seg"); for (int i = 0; i < this.cliente.getCarroCompra().length; i++) { // Se procesa el pedido en X segundos this.esperarXsegundos(cliente.getCarroCompra()[i]); System.out.println("Procesado el producto " + (i + 1) + " del " + this.cliente.getNombre()+ "->Tiempo: " + (System.currentTimeMillis() - this.initialTime) / 1000 + "seg"); } System.out.println("La cajera " + Thread.currentThread().getName() + " HA TERMINADO DE PROCESAR " + this.cliente.getNombre() + " EN EL TIEMPO: " + (System.currentTimeMillis() - this.initialTime) / 1000 + "seg"); } private void esperarXsegundos(int segundos) { try { Thread.sleep(segundos * 1000); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } // getter y setter
Por último mostramos el código de ejecución del proceso de compra:
public class MainExecutor { private static final int numCajeras = 2; public static void main(String[] args) { ArrayList<Cliente>clientes = new ArrayList<Cliente>(); clientes.add(new Cliente("Cliente 1", new int[] { 2, 2, 1, 5, 2 })); // 12 Seg clientes.add(new Cliente("Cliente 2", new int[] { 1, 1, 5, 1, 1 })); // 9 Seg clientes.add(new Cliente("Cliente 3", new int[] { 5, 3, 1, 5, 2 })); // 16 Seg clientes.add(new Cliente("Cliente 4", new int[] { 2, 4, 3, 2, 5 })); // 16 Seg clientes.add(new Cliente("Cliente 5", new int[] { 1, 3, 2, 2, 3 })); // 11 Seg clientes.add(new Cliente("Cliente 6", new int[] { 4, 2, 1, 3, 1 })); // 11 Seg clientes.add(new Cliente("Cliente 7", new int[] { 3, 3, 2, 4, 7 })); // 19 Seg clientes.add(new Cliente("Cliente 8", new int[] { 6, 1, 3, 1, 3 })); // 14 Seg // Tiempo total en procesar todos los pedidos = 108 segundos long init = System.currentTimeMillis(); // Instante inicial del procesamiento ExecutorService executor = Executors.newFixedThreadPool(numCajeras); for (Cliente cliente: clientes) { Runnable cajera = new CajeraRunnable(cliente, init); executor.execute(cajera); } executor.shutdown(); // Cierro el Executor while (!executor.isTerminated()) { // Espero a que terminen de ejecutarse todos los procesos // para pasar a las siguientes instrucciones } long fin = System.currentTimeMillis(); // Instante final del procesamiento System.out.println("Tiempo total de procesamiento: "+(fin-init)/1000+" Segundos"); } }
En este fragmento de código es donde se gestiona toda la ejecución de los threads. En primer lugar hemos creado 8 objetos Cliente y los hemos metido en un ArrayList llamado clientes. Luego hemos puesto lo siguiente:
ExecutorService executor = Executors.newFixedThreadPool(numCajeras);
Hemos creado un objeto de la clase Executors en el que en el método «newFixedThreadPool()» le estamos diciendo que fije como número de threads a ejecutar ‘2’ (siendo numCajeras = 2); es decir, que nos llevará la gestión de los procesos a ejecutar en los threads del programa. Lo de «ThreadPool» es porque este Framework Executors parte del concepto de que gestiona una piscina (pool) de threads, de ahi que a los threads que se crea les asigne nombres del tipo «pool-1-thread-1«, «pool-1-thread-2«, etc.
Una vez declarado el «executor», lanzamos los procesos (es decir los clientes se ponen en cola) para procesarlos. Para ello recorremos el ArrayList y decimos que a cada Cliente le procese el pedido una cajera (Runnable cajera = new CajeraRunnable(cliente, init)) y decimos también que sea el Executor el encargado de gestionar la cola de threads (executor.execute(cajera)):
for (Cliente cliente: clientes) { Runnable cajera = new CajeraRunnable(cliente, init); executor.execute(cajera); }
De esta forma lo que estamos diciendo al objeto «executor» es que tiene que procesar a los 8 Clientes del ArrayList y que se encargará de procesarlos en orden, utilizando dos threads o hilos; es decir, que se encargue de gestionar la cola de procesos. Como resultado de la ejecución del programa, obtenemos los siguiente resultados (con 2 threads):
"La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 1 EN EL TIEMPO: 0seg "La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 2 EN EL TIEMPO: 0seg Procesado el producto 1 del Cliente 2->Tiempo: 1seg Procesado el producto 1 del Cliente 1->Tiempo: 2seg ............... Procesado el producto 4 del Cliente 2->Tiempo: 8seg Procesado el producto 5 del Cliente 2->Tiempo: 9seg "La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 2 EN EL TIEMPO: 9seg "La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 3 EN EL TIEMPO: 9seg Procesado el producto 4 del Cliente 1->Tiempo: 10seg Procesado el producto 5 del Cliente 1->Tiempo: 12seg "La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 1 EN EL TIEMPO: 12seg "La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 4 EN EL TIEMPO: 12seg Procesado el producto 1 del Cliente 3->Tiempo: 14seg Procesado el producto 1 del Cliente 4->Tiempo: 14seg ............... Procesado el producto 4 del Cliente 4->Tiempo: 23seg Procesado el producto 5 del Cliente 3->Tiempo: 25seg "La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 3 EN EL TIEMPO: 25seg "La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 5 EN EL TIEMPO: 25seg Procesado el producto 1 del Cliente 5->Tiempo: 26seg Procesado el producto 5 del Cliente 4->Tiempo: 28seg "La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 4 EN EL TIEMPO: 28seg "La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 6 EN EL TIEMPO: 28seg Procesado el producto 2 del Cliente 5->Tiempo: 29seg ............... Procesado el producto 5 del Cliente 5->Tiempo: 36seg "La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 5 EN EL TIEMPO: 36seg "La cajera pool-1-thread-2" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 7 EN EL TIEMPO: 36seg Procesado el producto 4 del Cliente 6->Tiempo: 38seg Procesado el producto 1 del Cliente 7->Tiempo: 39seg Procesado el producto 5 del Cliente 6->Tiempo: 39seg "La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 6 EN EL TIEMPO: 39seg "La cajera pool-1-thread-1" COMIENZA A PROCESAR LA COMPRA DEL CLIENTE Cliente 8 EN EL TIEMPO: 39seg Procesado el producto 2 del Cliente 7->Tiempo: 42seg ............... Procesado el producto 5 del Cliente 8->Tiempo: 53seg "La cajera pool-1-thread-1" HA TERMINADO DE PROCESAR Cliente 8 EN EL TIEMPO: 53seg Procesado el producto 5 del Cliente 7->Tiempo: 55seg "La cajera pool-1-thread-2" HA TERMINADO DE PROCESAR Cliente 7 EN EL TIEMPO: 55seg Tiempo total de procesamiento: 55 Segundos
Otra cosa muy importante es «apagar» el executor, ya que aunque le mandemos ejecutar solo 8 Clientes, va a estar todo el rato preguntando (haciendo polling) si hay más procesos que ejecutar; por tanto, podéis comprobar como quitando del código el «executor.shutdown()» el programa no terminará de ejecutarse.
También debéis de controlar la secuencia del programa y si queréis que hasta que no termine la ejecución de los procesos que ejecuta el «executor» no ejecute los siguientes, debéis de escribir un bucle «while» preguntando constantemente si ha concluido la ejecución del executor o no de la siguiente manera:
while (!executor.isTerminated()) { // Espero a que terminen de ejecutarse todos los procesos // para pasar a las siguientes instrucciones }
Sino controlásemos esto, el «executor» ejecutaría los procesos asignados por un lado y el programa seguiría su ejecución.
En resumen esta es la forma más sencilla de gestionar una cola de procesos de deben de ser ejecutados en un número determinado de threads. En este ejemplo al haber 8 clientes, podríamos haber ejecutado los 8 Clientes es paralelo; es decir, poniendo «numcajeras = 8» y el tiempo de ejecución del programa hubiese sido de 19 segundos que es el tiempo que tardaría en procesarse el cliente más costoso y no hubiese pasado nada ya que Java gestiona bastante bien los threads y aunque nuestro PC tenga 2 núcleos puede ejecutar 8 threads o más a la vez sin que nos demos cuenta, pero pensar que si en vez de ejecutar 8 Clientes ejecutásemos 2 o 3 millones de clientes no podríamos lanzar 2 o 3 millones de threads ya que se produciría un «trasiego» espectacular y tardaría mucho más el sistema operativo en asignar CPU a los 2 o 3 millones procesos que en ejecutarlos, por tanto es muy importante saber gestionar bien un buen sistema de colas de procesos para poder ejecutar en paralelo los más posibles y en Java el Framework Executor nos permite hacerlo de forma muy sencilla como se ha visto en esta entrada.
Muy práctico tu ejemplo. Me puedes ayudar recién empiezo esto de Hilos Callable.
Cómo se puede sumar a cada cajera la cantidad de clientes que atendió. Y luego mostrarlo en un println.
Buenas Paola.
Dentro de CajeraRunnable podríamos tener un contador para que cada «cajera» lleve la cuenta de los clientes y al terminar lo muestre en un «System.out.print» (donde pinta «HA TERMINADO DE PROCESAR»)
Hola, excelente artículo. Gracias como siempre… muy precisos e ilustrativos en sus explicaciones.
Una pregunta:
¿Cómo garantizar que una aplicación multithreading acceda de forma correcta a una variable (a nivel de método)?
¿Cómo garantizar que una aplicación multithreading acceda de forma correcta a una variable (en el interior de un método)?
Y si fuese el usuario quien introduciera el numero de cajas y de clientes que quire?
Buenos dias, quiero aplicar en la gestion de colas, pero ya no trabajar en seg sino en minutos que es el estandar en los estudios
de tiempos y movimientos, para eso haria una escala .Otro seria que los clientes arriben a la estacion segun una distribucion estadisticas para asi llenar los arraylist, etc bien gracias por compartir
Nota; me puedes dar ejemplos o alguna referencia de Graficos Gant en Java.
Hola Ricardo, muchas gracias por el material compartido. Un favor si tienes links con mas informacion sobre concurrencia y paralelismo.
Muy claro, felicitaciones por el gran trabajo!
El tiempo total de procesamineto no es el correcto.
¿Cómo debería cacularse dicho tiempo?
Hola buen dia me interesa mucho java, yo utilizo j creator , y tambien me gustaria me mandaran ejemplos de hilos que corran en jcreator.
Muchas gracias
Jesus,
Java es java el jcreator, netbeans, eclipse, sts, etc etc son herramientas que se utilizan para programar Java y otros lenguajes.
El codigo java es unico por lo que puedes utilizar en jcreator y en las otras herramientas de programacion.
saludos
Quisiera ser parte de su equipo, ya que las «entradas» y el contenido general del sitio me parecen muy interesantes, me gustaría ser participe de el y ya que llevo ya casi 4 años en el mundo de la informática, además de ser un estudiante Universitario, cuya carrera es Ingeniería en Sistemas Computacionales, actualmente estoy en el desarrollo de Aplicaciones Móviles y Web.
Si por algún motivo requieren ayuda para el soporte del sitio u otra situación estaría encantado de apoyarles.