Multitarea e Hilos en Java con ejemplos II (Runnable & Executors)


Java_Runnable_executor_jarrobaEl 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.

Comparte esta entrada en:
Safe Creative #1401310112503
Multitarea e Hilos en Java con ejemplos II (Runnable & Executors) por "www.jarroba.com" esta bajo una licencia Creative Commons
Reconocimiento-NoComercial-CompartirIgual 3.0 Unported License.
Creado a partir de la obra en www.jarroba.com

9 comentarios en “Multitarea e Hilos en Java con ejemplos II (Runnable & Executors)”

  1. 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)?

  2. 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.

  3. Hola Ricardo, muchas gracias por el material compartido. Un favor si tienes links con mas informacion sobre concurrencia y paralelismo.

  4. 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

    1. 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

  5. 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.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies

ACEPTAR