Un hilo es un flujo de control dentro de un programa que permite realizar una tarea separada. Es decir, creando varios hilos podremos realizar varias tareas simultáneamente. Cada hilo tendrá sólo un contexto de ejecución (contador de programa, pila de ejecución). Es decir, a diferencia de los procesos UNIX, no tienen su propio espacio de memoria sino que acceden todos al mismo espacio de memoria común, por lo que será importante su sincronización cuando tengamos varios hilos accediendo a los mismos objetos.
En Java los hilos están encapsulados en la clase Thread. Para crear un hilo tenemos dos posibilidades:
En ambos casos debemos definir un método run() que será el que contenga el código del hilo. Desde dentro de este método podremos llamar a cualquier otro método de cualquier objeto, pero este método run() será el método que se invoque cuando iniciemos la ejecución de un hilo. El hilo terminará su ejecución cuando termine de ejecutarse este método run().
Para crear nuestro hilo mediante herencia haremos lo siguiente:
public class EjemploHilo extends Thread { public void run() { // Código del hilo } }
Una vez definida la clase de nuestro hilo deberemos instanciarlo y ejecutarlo de la siguiente forma:
Thread t = new EjemploHilo(); t.start();
Al llamar al método start del hilo, comenzará ejecutarse su método run (notar que no se llama a run directamente). Crear un hilo heredando de Thread tiene el problema de que al no haber herencia múltiple en Java, si heredamos de Thread no podremos heredar de ninguna otra clase, y por lo tanto un hilo no podría heredar de ninguna otra clase.
Este problema desaparece si utilizamos la interfaz Runnable para crear el hilo, ya que una clase puede implementar varios interfaces. Definiremos la clase que contenga el hilo como se muestra a continuación:
public class EjemploHilo implements Runnable { public void run() { // Código del hilo } }
Para instanciar y ejecutar un hilo de este tipo deberemos hacer lo siguiente:
Thread t = new Thread(new EjemploHilo()); t.start();
Esto es así debido a que en este caso EjemploHilo no deriva de una clase Thread, por lo que no se puede considerar un hilo, lo único que estamos haciendo implementando la interfaz es asegurar que vamos a tener definido el método run(). Con esto lo que haremos será proporcionar esta clase al constructor de la clase Thread, para que el objeto Thread que creemos llame al método run() de la clase que hemos definido al iniciarse la ejecución del hilo, ya que implementando la interfaz le aseguramos que esta función existe.
Un hilo pasará por varios estados durante su ciclo de vida.
Thread t = new Thread(this);
Una vez se ha instanciado el objeto del hilo, diremos que está en estado de Nuevo hilo.
t.start();
Cuando invoquemos su método start() el hilo pasará a ser un hilo vivo, comenzándose a ejecutar su método run(). Una vez haya salido de este método pasará a ser un hilo muerto.
La única forma de parar un hilo es hacer que salga del método run() de forma natural. Podremos conseguir esto haciendo que se cumpla una condición de salida de run() (lógicamente, la condición que se nos ocurra dependerá del tipo de programa que estemos haciendo). Las funciones para parar, pausar y reanudar hilos están desaprobadas en las versiones actuales de Java.
Mientras el hilo esté vivo, podrá encontrarse en dos estados: Ejecutable y No ejecutable. El hilo pasará de Ejecutable a No ejecutable en los siguientes casos:
Lo único que podremos saber es si un hilo se encuentra vivo o no, llamando a su método isAlive().
Además, una propiedad importante de los hilos será su prioridad. Mientras el hilo se encuentre vivo, el scheduler de la máquina virtual Java le asignará o lo sacará de la CPU, coordinando así el uso de la CPU por parte de todos los hilos activos basándose en su prioridad. Se puede forzar la salida de un hilo de la CPU llamando a su método yield(). También se sacará un hilo de la CPU cuando un hilo de mayor prioridad se haga Ejecutable, o cuando el tiempo que se le haya asignado expire.
Para cambiar la prioridad de un hilo se utiliza el método setPriority(), al que deberemos proporcionar un valor de prioridad entre MIN_PRIORITY y MAX_PRIORITY (tenéis constantes de prioridad disponibles dentro de la clase Thread, consultad el API de Java para ver qué valores de constantes hay).
En cualquier parte de nuestro código Java podemos llamar al método currentThread de la clase Thread, que nos devuelve un objeto hilo con el hilo que se encuentra actualmente ejecutando el código donde está introducido ese método. Por ejemplo, si tenemos un código como:
public class EjemploHilo implements Runnable { public EjemploHilo() { ... int i = 0; Thread t = Thread.currentThread(); t.sleep(1000); } }
La llamada a currentThread dentro del constructor de la clase nos devolverá el hilo que corresponde con el programa principal (puesto que no hemos creado ningún otro hilo, y si lo creáramos, no ejecutaría nada que no estuviese dentro de un método run.
Sin embargo, en este otro caso:
public class EjemploHilo implements Runnable { public EjemploHilo() { Thread t1 = new Thread(this); Thread t2 = new Thread(this); t1.start(); t2.start(); } public void run() { int i = 0; Thread t = Thread.currentThread(); t.sleep(1000); } }
Lo que hacemos es crear dos hilos auxiliares, y la llamada a currentThread se produce dentro del run, con lo que se aplica a los hilos auxiliares, que son los que ejecutan el run: primero devolverá un hilo auxiliar (el que primero entre, t1 o t2), y luego el otro (t2 o t1).
Como hemos visto en los ejemplos anteriores, una vez obtenemos el hilo que queremos, el método sleep nos sirve para dormirlo, durante los milisegundos que le pasemos como parámetro (en los casos anteriores, dormían durante 1 segundo). El tiempo que duerme el hilo, deja libre el procesador para que lo ocupen otros hilos. Es una forma de no sobrecargar mucho de trabajo a la CPU con muchos hilos intentando entrar sin descanso.
Implementaremos ahora un ejercicio para practicar diferentes aspectos sobre hilos y multiprogramación. Echa un vistazo a la clase sesion05.Ej2 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, que se encarga de liberar memoria no usada. 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.
¿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). ¿Qué pasaría si no estuviese el bucle do...while en el constructor del programa principal?
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é?
Observa si los hilos terminan en el orden establecido. ¿Existe esta vez más diferencia de tiempos en el orden de finalización?
Muchas veces los hilos deberán trabajar de forma coordinada, por ejemplo para acceder a una misma variable, o a un mismo fichero, por lo que es necesario un mecanismo de sincronización entre ellos.
Un primer mecanismo de comunicación es una variable cerrojo incluida en todo objeto Object, que permitirá evitar que más de un hilo entre en una determinada sección crítica de código para un objeto determinado. Los métodos declarados como synchronized utilizan el cerrojo del objeto al que pertenecen evitando que más de un hilo entre en ellos al mismo tiempo.
public synchronized void seccion_critica() { // Código sección crítica }
Todos los métodos synchronized de un mismo objeto (no clase, sino objeto de esa clase), comparten el mismo cerrojo, y es distinto al cerrojo de otros objetos (de la misma clase, o de otras).
También podemos utilizar cualquier otro objeto para la sincronización dentro de nuestro método de la siguiente forma:
synchronized (objeto_con_cerrojo) { // Código sección crítica }
de esta forma sincronizaríamos el código que escribiésemos dentro, con el código synchronized del objeto objeto_con_cerrojo.
Además podemos hacer que un hilo quede bloqueado a la espera de que otro hilo lo desbloquee cuando suceda un determinado evento. Para bloquear un hilo usaremos la función wait(), para lo cual el hilo que llama a esta función debe estar en posesión del monitor, cosa que ocurre dentro de un método synchronized, por lo que sólo podremos bloquear a un proceso dentro de estos métodos. Cuando un hilo queda bloqueado liberará el cerrojo para que otro hilo pueda entrar en la sección crítica del objeto y desbloquearlo.
Para desbloquear a los hilos que haya bloqueados se utilizará notifyAll(), o bien notify() para desbloquear sólo uno de ellos aleatoriamente. Para invocar estos métodos ocurrirá lo mismo, el hilo deberá estar en posesión del monitor. Notar que con notifyAll se despertarán todos, y "lucharán" por ver quién entra primero en el cerrojo. Notar también que no existen métodos para despertar un hilo concreto, sino que se despertarán todos (notifyAll), o bien uno al azar (notify).
Por último, puede ser necesario esperar a que un determinado hilo haya finalizado su tarea para continuar. Esto lo podremos hacer llamando al método join() de dicho hilo, que nos bloqueará hasta que el hilo haya finalizado. Por ejemplo, si queremos esperar a que el hilo t termine para seguir con nuestro código, haremos algo como:
t.join();
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.
Echa un vistazo a la clase sesion05.Ej3 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.
A continuación sincronizaremos productores y consumidores para que funcionen adecuadamente.
public void produce(int valor) { /* Si hay datos disponibles esperar a que se consuman */ ... // Comprueba aquí si hay datos disponibles, y si los hay llama al método para esperar
/* Producir */ this.valor = valor; /* Ya hay datos disponibles */ /* Notificar de la llegada de datos a consumidores a la espera */ ... // Haz aquí las modificaciones necesarias para indicar que ya hay datos disponibles nuevamente,
// y para notificar a los demás hilos que lo necesiten de que ya pueden pasar a consumir
}¿Qué papel puede tener el método wait en este código?
¿Para qué se utiliza el flag disponible?
¿Qué efecto puede tener la llamada a notifyAll?¿Qué pasaría si no se llamase a este método?
public int consume() { /* Si no hay datos disponibles esperar a que se produzcan */ ... // Comprueba aquí si hay datos disponibles, y si no llama al método para esperar
/* Ya no hay datos disponibles */ /* Notificar de que el recipiente esta libre a productores en espera */ ... // Haz aquí las modificaciones necesarias para indicar que ya NO hay datos disponibles,
// y para notificar a los demás hilos que lo necesiten de que ya pueden pasar a producir más
return valor; }
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)
Los grupos de hilos nos permitirán crear una serie de hilos y manejarlos todos a la vez como un único objeto. Si al crear un hilo no se especifica ningún grupo de hilos, el hilo creado pertenecerá al grupo de hilos por defecto.
Podemos crearnos nuestro propio grupo de hilos instanciando un objeto de la clase ThreadGroup. Para crear hilos dentro de este grupo deberemos pasar este grupo al constructor de los hilos que creemos.
ThreadGroup grupo = new ThreadGroup("Grupo de hilos"); Thread t = new Thread(grupo,new EjemploHilo());
PARA ENTREGAR
Guarda en la carpeta modulo2 de tu CVS los siguientes elementos para esta sesión: