Sesión 7

1. Implementaremos ahora un ejercicio para practicar diferentes aspectos sobre hilos y multiprogramación:

  1. Antes de comenzar, lee la introducción del punto 2.2 (Hilos), y los apartados 2.2.1 (Creación de hilos) y 2.2.2 (Estado y propiedades de los hilos) del tema 2 de teoría.
  2. Echa un vistazo a la clase Ej2.java que se proporciona en la plantilla de la sesión. Verás que tiene una subclase llamada MiHilo que es un hilo. Tiene un campo nombre que sirve para dar nombre al hilo, y un campo contador. También hay un método run que itera el contador del 1 al 1000; en cada iteración hace un System.gc(), es decir, llama al recolector de basura de Java. Lo hacemos para consumir algo de tiempo en cada iteración.

    Desde la clase principal (Ej2), se crea un hilo de este tipo, y se ejecuta (en el constructor). También en el constructor hacemos un bucle do...while, donde el programa principal va examinando qué valor tiene el contador del hilo en cada momento, y luego duerme un tiempo hasta la próxima comprobación, mientras el hilo siga vivo.
  3. Compila y prueba el funcionamiento de la clase de la siguiente forma, comprobando que el hilo se lanza y ejecuta sus 1000 iteraciones.
    javac Ej2.java
    java Ej2

    ¿Cuántos hilos o flujos de ejecución hay en total? ¿Qué hace cada uno? (AYUDA: en todo programa al menos hay UN flujo de ejecución, que no es otro que el programa principal. Aparte, podemos tener los flujos secundarios (hilos) que queramos).

  1. Vamos a añadir y lanzar dos hilos más de tipo MiHilo en la clase Ej2. Para ello podemos copiar y pegar el código que crea y lanza el primer hilo, para hacer otros dos nuevos:
    public Ej2()
    {
    	MiHilo t = new MiHilo("Hilo 1");
    	MiHilo t2 = new MiHilo("Hilo 2");
    	MiHilo t3 = new MiHilo("Hilo 3");
    
    	t.start();
    	t2.start();
    	t3.start();
    
    	do
    	{
    		System.out.print("Hilo 1 = " + t.contador + 
    		                 ", Hilo 2 = " + t2.contador + 
    		                 ", Hilo 3 = " + t3.contador + "\r");
    		try {
    			Thread.currentThread().sleep(100);
    		} catch(InterruptedException e) { }
    
    	} while (t.isAlive() || t2.isAlive() || t3.isAlive());
    }

    El bucle do...while lo modificamos también, y ahora nos muestra cuánto vale el contador de cada hilo cada vez. Dentro del bucle, hacemos que el hilo actual (es decir, el programa principal), se duerma cada 100 ms, para dejar sitio a los hilos en el procesador.

    Ejecuta después el programa varias veces (3 o 4), y comprueba el orden en el que terminan los hilos. ¿Existe mucha diferencia de tiempo entre la finalización de éstos? ¿Por qué?

  2. Vamos a modificar la prioridad de los hilos, para hacer que terminen en orden inverso al que se lanzan. Para ello daremos al hilo 3 la máxima prioridad, al hilo 2 una prioridad normal, y al hilo 1 una prioridad mínima:
    public Ej2()
    {
    	MiHilo t = new MiHilo("Hilo 1");
    	MiHilo t2 = new MiHilo("Hilo 2");
    	MiHilo t3 = new MiHilo("Hilo 3");
    
    	t.setPriority(Thread.MIN_PRIORITY);
    	t2.setPriority(Thread.NORM_PRIORITY);
    	t3.setPriority(Thread.MAX_PRIORITY);
    
    	t.start();
    	t2.start();
    	t3.start();
    
    	...
    }

    Observa si los hilos terminan en el orden establecido. ¿Existe esta vez más diferencia de tiempos en el orden de finalización?

  3. Observa los campos locales de la clase MiHilo: la variable contador y el campo nombre. A juzgar por lo que has visto en la ejecución de los hilos: ¿Podría modificar el hilo t2 el valor de estos campos para el hilo t3, es decir, comparten los diferentes hilos estos campos, o cada uno tiene los suyos propios? ¿Qué pasaría si los tres hilos intentasen modificar el campo valor de la clase principal Ej2, también tendrían una copia cada uno o accederían los tres a la misma variable?

  4. (OPTATIVO) Crea una clase Ej2b similar a la anterior, pero en la que no sea el programa principal quien active los 3 hilos, sino que cada uno de los 3, el empezar a funcionar, cree e inicie el hilo siguiente (hasta un total de 3 hilos). De esta forma, se ejecutarán en paralelo, pero no será el programa principal quien los lance. Este sólo lanzará un hilo, que será el responsable de seguir la cadena.

2. En este ejercicio vamos a practicar con la sincronización entre múltiples hilos, resolviendo el problema de los productores y los consumidores. Vamos a definir 3 clases: el hilo Productor, el hilo Consumidor, y el objeto Recipiente donde el productor deposita el valor producido, y de donde el consumidor extrae los datos.

  1. Antes de comenzar, lee el apartado 2.2.3 (Sincronización de hilos) del tema 2 de teoría.
  2. Echa un vistazo a la clase Ej3.java que se proporciona en la plantilla de la sesión. Verás que tiene 3 subclases: una llamada Productor, que serán los hilos que se encarguen de producir valores, otra llamada Consumidor, que serán los hilos que se encargarán de consumir los valores producidos por los primeros, y una tercera llamada Recipiente, donde los Productores depositarán los valores producidos, y los Consumidores retirarán dichos valores.

    Los valores producidos no son más que números enteros, que los productores generarán y los consumidores leerán. 

    Si miramos el código de los hilos Productores, vemos (método run) que los valores que producen van de 0 a 9, y entre cada producción duermen una cantidad aleatoria, entre 1 y 2 segundos.

    En cuanto a los Consumidores, vemos que entre cada consumición también duermen una cantidad aleatoria entre 1 y 2 segundos, y luego consumen el valor.

    Para producir y para consumir se utilizan los métodos produce y consume, respectivamente, de la clase Recipiente.
  3. Compila y prueba varias veces (3 o 4) el funcionamiento de la clase.
    javac Ej3.java
    java Ej3

    ¿Funciona bien el programa? ¿Qué anomalía(s) detectas?

  4. Como habrás podido comprobar, el programa no funciona correctamente: hay iteraciones en las que el hilo Consumidor consume ANTES de que el Productor produzca el nuevo valor, o el Productor produce dos valores seguidos sin que el Consumidor los consuma. 

    Es necesario sincronizarlos de forma que el Consumidor se espere a que el Productor produzca, y luego el Productor espere a que el Consumidor consuma, antes de generar otro nuevo valor. Es decir, el funcionamiento correcto debería ser que el consumidor consuma exactamente los mismos valores que el productor ha producido, sin saltarse ninguno ni repetirlos.
  5. Vamos a añadir el código necesario en los métodos produce y consume para sincronizar el acceso a ellos. El comportamiento debería ser el siguiente:
    1. Si queremos producir y todavía hay datos disponibles en el recipiente, esperaremos hasta que se saquen, si no produciremos y avisamos a posibles consumidores que estén a la espera.
      public void produce(int valor) 
      {
      	/* Si hay datos disponibles esperar a que se consuman */
      
      	if(disponible) {
      		try {
      			wait();
      		} catch(InterruptedException e) { }
      	}
      
      	this.valor = valor;
      
      	/* Ya hay datos disponibles */
      
      	disponible = true;
      
       	/* Notificar de la llegada de datos a consumidores a la espera */
      
      	notifyAll();
      
      }
      ¿Qué hace el método wait en este código?
      ¿Para qué se utiliza el flag disponible?
      ¿Qué efecto tiene la llamada a notifyAll?¿Qué pasaría si no estuviese?
    2. Si queremos consumir y no hay datos disponibles en el recipiente, esperaremos hasta que se produzcan, si no consumimos el valor disponible y avisamos a posibles productores que estén a la espera.
      public int consume()
      {
      	/* Si no hay datos disponibles esperar a que se produzcan */
      
      	if(!disponible) {
      		try {
      			wait();
      		} catch(InterruptedException e) { }
      	}
      
      	/* Ya no hay datos disponibles */
      
      	disponible = false;
      
      	/* Notificar de que el recipiente esta libre a productores en espera */
      	notifyAll();
      
      	return valor;
      }
  6. Compilad y ejecutad el programa. ¿Qué excepción o error da? ¿A qué puede deberse?

  7. El error del paso anterior lo da porque no podemos llamar a los métodos wait o notify/notifyAll si no tenemos la variable cerrojo. Dicha variable se consigue dentro de bloques de código synchronized, de forma que el primer hilo que entra es quien tiene el cerrojo, y hasta que no salga o se ponga a esperar, no lo liberará. 

    Para corregir el error, debemos hacer que tanto produce como consume sean dos métodos synchronized, de forma que nos aseguremos que sólo un hilo a la vez entrará, y podrá ejecutar dichos métodos.

    public synchronized int consume() 
    {
    	...
    }
    
    public synchronized void produce (int valor)
    {
    	...
    }
  8. Compilar y comprobar que el programa funciona correctamente. Si lo ejecutáis varias veces, podría darse el caso de que aún salgan mensajes de consumición ANTES de que se muestren los de producción respectivos, por ejemplo:

    Produce 0
    Consume 0
    Consume 1
    Produce 1
    Produce 2
    Consume 2
    ...

    ... pero aún así el programa es correcto. ¿A qué se debe entonces que puedan salir los mensajes en orden inverso? (AYUDA: observad el método System.out.println(...) que hay al final de los métodos run de Productor y Consumidor, que es el que muestra estos mensajes... ¿hay algo que garantice que se ejecute este método en orden?)

PARA ENTREGAR