[icono]Manual de Java Volver a índice

Capítulo 8. Hilo y Sincronización.


La programación multihilo es un paradigma conceptual de la programación pro el cual se dividen los programs en dos o más procesos que se pueden ejecutar en paralelo. En un momento dado pueden haber datos de entrada de usuario a los que responder, animaciones y visualizaciones de interfaz de usuario, también cálculos grandes que podrían tardar varios segundos en terminar, y nuestros programas tendrán que tratar con estos temas sin provocar retrasos desagradables al usuario.

Lo interesante de todos estos procesos en paralelo es que la mayor parte de ellos realmente no necesitan los recursos completos de la computadora durante su vida operativa. El problema en los entornos de hilo único tradicionales es que se tiene que esperar a que se terminen cada una de estas tareas antes de proseguir con la siguiente. Aunque la CPU esté libre la mayor parte del tiempo, tiene que colocar las tareas en la cola ordenadamente.

El modelo de hilo de Java

Los sistemas multihilo aprovechan la circunstancia de que la mayoría de los hilos computacionales invierten la mayor parte del tiempo esperando a que un recurso quede disponible, o bien esperando a que se cumpla alguna condición de temporización. Si fuésemos capaces de describir todas las tareas como hilos de control independientes, conmutando de manera automática entre una tarea que esté lista para pasar a un modo de espera, y otra que sí tenga algo que hacer, conseguiríamos realizar una cantidad mayor de trabajo en le mismo intervalo de tiempo.

Java se diseño partiendo de cero, en un mundo en el que el entorno multihilo, a nivel de sistema operativo, era una realidad. El intérprete de Java hace uso intensivo de hilos para multitud de propósitos, y todas las bibliotecas de clases se diseñaron teniendo en mente el modelo multihilo. Una vez que un hilo comienza su tarea, puede suspenderse, lo que equivale a detener temporalmente su actividad. El hilo suspendido puede reanudarse, lo que supone que continúa su tarea allí donde la dejó. En cualquier momento, un hilo puede deteriores, finalizando su ejecución de manera inmediata. Una vez detenido, el proceso no puede reiniciarse.

Prioridades de hilo

El intérprete de Java utiliza prioridades para determinar cómo debe comportarse cada hilo con respecto a los demás. Las prioridades de hilo son valores entre 1 y 10 que indican la prioridad relativa de un hilo con respecto a los demás.

Sincronización

Ya que los hilos permiten y potencian el comportamiento asíncrono de los programas, debe existir alguna manera de forzar el sincronismo allí donde sea necesario. Por ejemplo, si desease que dos hilos se comunicasen para compartir una estructura de datos compleja (como una lista enlazada), necesitará alguna manera de garantizar que cada uno se aparte del camino del otro. Java incorpora una versión rebuscada de un modelo clásico para la sincronización, el monitor. La mayor parte de los sistemas multihilo implementan los monitores a modo de objetos, pero Java proporciona una solución más elegante: no existe la clase monitor, cada objeto lleva asociado su propio monitor implícito, en el que puede entrar sin más que hacer una llamada a los métodos synchronized del objeto. Una vez que el hilo está dentro del método synchronized, ningún otro hilo puede efectuar una llamada a otro método synchronized sobre el mismo objeto.

Intercambio de mensajes

Una vez que el programa se ha dividido en sus partes lógicas, a modo de hilo, es preciso definir exactamente como se comunicarán entre si dichos hilos. Java utiliza los métodos wait y notify para el intercambio de información entre hilos.

Thread

En Java los hilos se representa mediante una clase. La clase Thread encapsula todo el control necesario sobre los hilos. Hay que tomar la precaución de distinguir claramente un objeto Thread de un hilo en ejecución. Un objeto Thread se define como el panel de control o proxy de un hilo en ejecución. En el objeto Thread hay métodos que controlan si el hilo se está ejecutando, está durmiendo, en suspenso o detenido. La clase Thread es la única manera de controlar el comportamiento de los hilos. En la siguiente instrucción se muestra como acceder al hilo en ejecución actual:

Thread t = Thread.currentThread(); * el hilo actual se almacena en la variable t *

Runnable

Si queremos tener más de un hilo necesitamos crear otra instancia de Thread. Cuando construimos una nueva instancia de Thread, necesitamos decirle que código ejecutar en el nuevo hilo de control. Se puede comenzar un hilo sobre cualquier objeto que implemente la interfaz Runnable.
Runnable es una interfaz simple que abstrae la noción de que se desea que algún código se "ejecute" asíncronamente. Para implementar Runnable, a una clase le basta con implementar un solo método llamado run. Este es un ejemplo que crea un nuevo hilo.

class ThreadDemo implements Runnable  {
	ThreadDemo()  {
		Thread ct = Thread.currentThread();
		Thread t = new Thread(this, "demo Thread");
		System.out.println("hilo actual: " + ct);
		System.out.println("Hilo creado: " + t);
		t.start();
		try  {
			Thread.sleep(3000);
		}  catch (interrupteExecption e)  {
		   	System.out.println("Interrumpido");
		}
		System.out.println(!saliendo del hilo main");
    	}
    	public void run()  {
		try  {
			for >(int y = 5; y > 0; y--)  {
				System.out.println(" " + i);
				Thread.sleep(1000);
		 	}
		}  catch (InterruptedException e)  {
			System.out.println("hijo interrumpido");
		}
		System.out.println("saliendo del hilo hijo");
    	}
    	public static void main (String args [])  {
		new ThreadDemo();
	}
}

El hilo main crea un nuevo objeto Thread, con new Thread (this, "Demo Thread"), pasando this como primer argumento para indicar que queremos que el nuevo hilo llame al método run sobre este (this) objeto. A continuación llamamos a start, lo que inicia el hilo de la ejecución a partir del método run. Después, el hilo main se duerme durante 3000 milisegundos antes de imprimir un mensaje y después termina. Demo Thread todavía está contando desde cinco cuando sucede esto. Se continúa ejecutando hasta que termina con el bucle de run. Esta es la salida después de cinco segundos:

C:\> java ThreadDemo
Hilo actual: Thread[main, 5, main]
Hilo creado: Thread[demo Thread, 5, main]
5
4
3
saliendo del hilo main
2
1
saliendo del hilo hijo

Prioridades de los hilos

El planificador de hilos hace uso de las prioridades de los mismos para decidir cuándo debe dejar a cada hilo que se ejecute, de manera que los hilos con mayor prioridad deben ejecutarse más a menudo que lo de menor prioridad. Cuando está ejecutándose un hilo de baja prioridad, y otro de mayor prioridad se despierta de su sueño, o de la espera por un operación de E/S, debe dejarse que se ejecute de manera inmediata, desalojando al hilo de menor prioridad. Cuando los hilos son de igual prioridad deben desalojarse los unos a los otros, cada cierto tiempo, utilizando el algoritmo circular round-robin para gestionar el acceso al la CPU.

En JDK 1.0 la planificación de hilos es un problema que no está completamente resuelto. Por lo que si pretendemos tener un comportamiento predecible sobre las aplicaciones deberemos utilizar hilos que, voluntariamente, cedan el control de la CPU.

Sincronización

Cuando dos o más hilos necesitan acceder de manera simultánea a un recurso de datos compartido necesitan asegurarse de que sólo uno de ellos accede al mismo cada vez. Java proporciona un soporte único, el monitor, es un objeto que se utiliza como cerrojo exclusivo. Solo uno de los hilos puede ser el propietario de un monitor en un instante dado. Los restantes hilos que estuviesen intentando acceder al monitor bloqueado quedan en suspenso hasta que el hilo propietario salga del monitor.

Todos los objetos de Java disponen de un monitor propio implícitamente asociado a ellos. La manera de acceder a un objeto monitor es llamando a un método marcado con la palabra clave synchronized. Durante todo el tiempo en que un hilo permanezca en un método sincronizado, los demás hilos que intenten llamar a un método sincronizado sobre la misma instancia tendrán que esperar. Para salir del monitor y permitir el control del objeto al siguiente hilo en espera, el propietario del monitor sólo tiene que volver del método

La sentencia synchronized

Si se utiliza una clase que no fue diseñada para accesos multihilo y, por ello, dispone de métodos no sincronizados que manipulan el estado interno, puede envolver la llamada al método en un bloque sincronizado. El formato general de la sentencia sincronizada es el siguiente:

synchronized(objeto) sentencia;

En el ejemplo, objeto es cualquier referencia al objeto, y sentencia suele ser un bloque que incluye una llamada al método de objeto, que solo tendrá lugar una vez que el hilo haya entrado con éxito en el monitor de objeto. Ahora veremos las formas de sincronización con un ejemplo:

class Callme {
	void call (String msg) { * también podía haber puesto synchronized antes de void *
		System.out.print("[" + msg);
		try Thread.sleep(1000); catch (Exception e);
		System.out.println("]");
    	}
}
class caller implements Runnable {
	String msg;
    	Callme target;
    	public caller(Callme t, String s) {
		target = t;
		msg = s;
		new Thread(this).start();
    	}
    	public void run() {
		synchronized(target) {
			target.call(msg);
		}
    	}
}
class Synch {
    	public static void main(String args[]) {
		Callme target = new Callme();
		new caller(target, "Hola");
		new caller(target, "Mundo");
		new caller(target, "Sincronizado");
    	}
}

Este programa imprime por pantalla el literal "Hola Mundo Sincronizado", cada palabra en una línea y entre comillas, se crea una instancia de Callme y tres instancias de caller que cada una de ellas referencia al mismo Callme con lo que necesitamos de una sincronización para el acceso a Callme, pues sino se mezclarían las tres llamada al haber una sentencia sleep que retrasa la ejecución de Callme dando lugar a que antes de que acabe un proceso deje libre el acceso a dicho objeto.

Comunicación entre hilos

Veamos, por ejemplo, el problema clásico de las colas, donde uno de los hilos produce datos y otro los consume. Para que el problema sea más interesante supongamos que el productor tiene que esperar a que el consumidor haya terminado, para empezar a producir más datos. En un sistema basado en sondeo el consumidor estaría desperdiciando ciclos de CPU mientras espera a que el productor produzca. Una vez que el productor ha terminado, se queda sondeando hasta ver que el consumidor ha finalizado, y así sucesivamente.

Evidentemente, hay una forma mejor de hacerlo. Java proporciona un mecanismo elegante de comunicación entre procesos, a través de los métodos wait, notify y notifyAll. Estos métodos se implementan como métodos de final en Object, de manera que todas las clases disponen de ellos. Cualquiera de los tres métodos sólo puede ser llamado desde dentro de un método synchronized.

El ejemplo de el productor y el consumidor es en Java como sigue:

class Q {
	int n;
    	boolean valueSet = false;
    	synchronized int get() {
		if (!valueSet)
	    		try wait(); catch(InterruptedException e);
		System.out.println("Obtenido: " + n);
		valueSet = false;
		notify();
		return n;
    	}
    	synchronized void put(int n) {
		if (valueSet)
	    		try wait(); catch(InterruptedException e);
		this.n = n;
		valueSet = true;
		System.out.println("Colocado: " + n);
		notify();
    	}
}
class Producer implements Runnable {
    	Q q;
    	Producer (Q q) {
		this.q =  q;
		new Thread(this, "Producer").start();
    	}
    	public void run() {
		int y = 0;
		while(true) {
	    		q.put(i++);
		}
    	}
}
class Consumer implements Runnable {
    	Q q;
    	Consumer(Q q) {
		this.q = q;
		new Thread(this, "Consumer").start();
    	}
    	public void run() {
		while(true) {
	    		q.get();
		}
    	}
class PC {
    	public static void main(String args[]) {
		Q q = new Q();
		new Producer(q);
		new Consumer(q);
    	}
}

Bloqueos

Los bloqueos son condiciones anómalas inusuales, pero muy difíciles de depurar, donde dos hilos presentan una dependencia circular sobre un par de objetos sincronizados. Por ejemplo, si un hilo entra en el monitor sobre el objeto X y otro hilo entra en le monitor sobre el objeto Y, y si X intenta llamar a cualquier método sincronizado sobre Y, tal y como cabe esperar quedará detenido. Sin embargo, si Y, por su parte, intenta llamar a cualquier método sincronizado con X, entonces quedará esperando indefinidamente, ya que para conseguir el cerrojo de X tendría antes que liberar su propio cerrojo en Y, con el fin de que el primer hilo pudiera completarse.

Resumen de la interfaz de programación (API) de hilos

Se incluye a continuación una referencia rápida a todos los métodos de la clase Thread que se han comentado en este capítulo.

Métodos de clase

Estos son los métodos estáticos que deben llamarse de manera directa en la clase Thread.

Métodos de instancia