índice

LOS THREADS

�QUÉ ES UN THREAD ?

EJEMPLOS

LA HERENCIA MÚLTIPLE

ESTADOS DE UN THREAD

PRIORIDADES

COMPARTIR DATOS

LOS THREADS DEMONIOS

LOS GRUPOS DE THREADS

DIFICULTADES DE IMPLEMENTACIÓN

CONCLUSIÓN







¿QUÉ ES UN THREAD?

Un proceso es una unidad de ejecucuión que tiene un fin funcional, un espacio de memoria y líneas de código que describen su desarrollo. Las rutinas (también llamadas funciones o métodos, según el lenguaje), son tramos de tiempo de este proceso.

Pues bien, los Threads son algo intermedio entre estos conceptos, son una especie de procesos en el interior de un proceso.

La llamada a un thread es como la llamada a un método que devolvería seguidamente el control, aunque en realidad no ha terminado de ejecutarse.

Los threads forman parte del paquete Java.lang.

INTERÉS DE LOS THREADS RESPECTO A LOS MÉTODOS

Los threads permiten ejecutar tareas en paralelo. Por ejemplo, si se crea un programa para gestionar varias vías de comunicación simultáneamente, se puede elegir entre:
volver


INTERÉS DE LOS THREADS RESPECTO A LOS PROCESOS

Se trata de tener un espacio de memoria compartido, tanto de código compartido como de recursos.

De igual manera, el lanzamiento y la ejecución de un thread son mucho más económicos que el lanzamiento y la ejecución de un proceso.

volver


EJEMPLOS

UN EJEMPLO HISTÓRICO

	class Fabula {
		static Animal laLiebre;
		static Animal laTortuga;
		public static void main (String arg [ ] ) {
			laLiebre= new Animal ( 5, "L" );
			laTortuga= new Animal ( 1, "T" );
			laTortuga.run ( );
			laLiebre.run ( );
		}
	}

	class Animal {
		int miVelocidad;
		String miNombre;
		public Animal ( int laVelocidad, String elNombre ) {
			miNombre= elNombre;
			miVelocidad= laVelocidad;
		}

		public void sleep ( int tiempo ) {
			for ( int i= 0; i< tiempo; i++ ) { ; }
		};
	
		public void run ( ) {
			for ( int i= 0; i< 10; i++ ) {
				System.out.print ( miNombre );
				sleep ( 1000/miVelocidad );
			}
			System.out.println ( "\n" + miNombre + " ha llegado" );
		}
	}

Si se ejecutase este programa, saldría algo parecido a ésto:

	TTTTTTTTTT 
	T ha llegado 
	LLLLLLLLLL 
	L ha llegado

En este ejemplo, laLiebre no tiene ninguna oportunidad de llegar en primer lugar, porque el programa no se interesa por ell hasta que ha llegado laTortuga.

Si transformamos la clase Animal en thread, permitirá conocer varias ejecuciones simultáneas:

	class Fabula {
		static Animal laLiebre;
		static Animal laTortuga;
		public static void main ( String arg [ ] ) throws InterruptedException {
			// ignoramos por ahora "throws InterruptedException"
			// que se explica en el cap�tulo sobre las Excepciones
			laLiebre= new Animal ( 5, " L" );
			laTortuga= new Animal ( 1, " T" );
			laTortuga.start ( );
			laLiebre.start ( );
			// hay que esperar que los threads lanzados por el programa
			// principal hayan terminado su tarea antes de salir del
			// programa principal; se espera mediante
			// el metodo join de la clase Thread
			laTortuga.join ( );
			laLiebre.join ( );
		}
	}

	class Animal extends Thread {
		String miNombre;
		int miVelocidad;
		public  Animal ( int laVelocidad, String elNombre ) {
			miNombre= elNombre;
			miVelocidad= laVelocidad;
		}

		public void run ( ) {
			for ( int i= 0; i< 10; i++ ) {
				System.out.print ( miNombre ) ;
				try { // try se explica en el capitulo de las Excepciones
					excepciones
					sleep ( 1000/miVelocidad );
				}
				catch ( Exception e ) { ; } // lo mismo para catch
			}
			System.out.println ( "\n" + miNombre + " ha llegado );
		}
	}

A diferencia que el anterior ejemplo, la clase Animal hereda de Thread y la llamada a run ( ), m&deacute;todo de Animal, ha sido reemplazado por la llamada a start ( ), método de Thread.

La ejecución de este programa da como resultado:

	TLLLLLTLLLLL
	L ha  llegado
	TTTTTTTT
	T ha llegado

En efecto, el thread Tortuga pasa el control inmediatamente tras su lanzamiento, lo que permite al programa principal lanzar el thread Liebre justo después. Este último tiene una velocidad mucho más elevada y llega ampliamente en cabeza.

Sin embargo se observa que la Tortuga sigue avanzando mientras que la Liebre gana la carrera. En efecto, figura una T en la lista de las L, los dos threads se ejecuatan concurrenmtemente, aunque la Liebre consuma más recursoso, y llega pues más rápido.

Si se hubiera querido utilizar procesos en lugar de threads, habrían aparecido los siguientes problemas:


volver


� POR QUÉ START Y NO RUN ?

La llamada al método start ( ) de la clase Thread arranca el thread como tal ( y no como método ) y entra�a seguidamente de modo implícito la llamada al método run ( ).

volver


LA HERENCIA MÚLTIPLE

Esta implementación de los threads es muy interesante pero si se quiere crear un thread que herede de otra clase distinta de Thread, por ejemplo, Applet, no se puede. Esto restringe considerablemente el empleo de los threads.

Para remediar esta imposibilidad, los dise�adores del lenguaje Java han previsto un segundo mecanismo de lanzamiento de threads. Si las acciones de nuestro thread se describen en la clase Animal, podemos crear un Thread y darle como parámetro a su constructor un objeto del tipo Animal.

En lugar de:

	new Animal ( );
	Animal.start ( );
	/* . . . */
	class Animal extends Thread {
tendremos
	Thread T= new Thread ( new Animal  ( ) ) ;
	T.start ( );
Sí, pero entonces, �cuál es el método de Animal llamado en start ( ) ? Es de nuevo el método run ( ).

Cierto, pero si se pasa como parámetro un objeto definido por una clase que no tenga método run ( ), � habría un error en la ejecución ? Siempre es preferible tener un error en la compilación. La interfaz del constructor debiera parecerse a ésto:

	Thread ( CualquierClaseQueImplementeRun objeto )
¿Cómo definir una CualquierClaseQueImplementeRun ? La solución reside en el empleo de la interfaz de Runnable. El mecanismo de interfaz define clases totalmente abstractas : son especificaciones de clases. Una clase que declare heredar de una interfaz debe implementar todos los métodos definidos en la intarfaz. Por el contrario, implementar una ineterfaz no impide en absoluto heredar de una clase.

Existe una interfaz llamada Runnable que describe los métodos que deben implementar los objetos que puedan ser pasados como parámetro a constructores de threads. Una clase que implemente la interfaz Runnable tiene por obligación proporcionar una implementación del método run ( ), pero tiene como derecho ser pasada como parámetro en la creación de un thread. En la llamada a start ( ) del thread, el método run ( ) de la clase que implemente Runnable será llamado :

	import java.io.* ;
	
	class PruebaRunnable {  
		public static void main (String argv [ ] ) throws
		InterruptedException {  
			Thread A = new Thread ( new Animal ( ) ) ;  
			A.start ( ) ;  
			A.join ( ) ;  
		}  
	}  

	class Animal implements Runnable {  
		public Animal ( ) {  }  
		public void run ( ) {  
			for (int i = 0 ; i < 10 ; i++) {  
				System.out.print ("i") ;  
			};  
			System.out.print ("\n") ;  
		}  
	}
  

Para verificar el interés de la clase Runnable, intentemos compilar el ejemplo siguiente, reeemplazando :

	class Animal implements Runnable {

por

	class Animal /* impliments Runnable */ {
  

El compilador mostrará :

	runnable.java :5 : No constructor matching
	Tread (Animal)  
	found in class java.lang.Thread.  
	Thread A= new thread (new Animal ( ) ) ;

En efecto, cuando se va a llamar al método start ( ) del thread, éste intentará lanzar el método run ( ) el objeto que se pasa como parámetro en su constructor. El thread no está seguro de que este método exista a menos que el objeto pasado como parámetro implemente la interfaz de la clase Runnable.

Hay dos téctnicas diferentes para impliemtnar los threads :

En ambos casos, la clase debe definir el método run ( ), que se llama cuando se llama al método start ( ) del thread.

Conceptualmente heredar de Thread parece la mejor solución porque un objeto que tenga un método run( ) es un thread. Por tanto se heredará de Thread cuando ello sea posible y se implementará Runnable cuando la clase herede de otra clase.

volver


ESTADOS DE UN THREAD

Los threads son objetos que tienen un atributo predefinido : su estado de ejecución. Este no es directamente accesible como lo es habitualmente un atributo de objeto.

Como los procesos, los threads tienen un estado relativo a su ejecución ; pueden ser :

THREAD CREADO

Los threads antes que nada se crean, como cualquier otro objeto Java :

	laTortuga = new Animal (1, "T");
Esta instrucción crea el objeto, pero no asigna más recursos que la memoria necesaria. El thread está en el limbo, pero aún no está activo.
volver


THREAD ACTIVO

El método:

	laTortuga.start ( );
asigna los diferentes recursos necesarios para la ejecución del thread, y seguidamente lanza el método :
	run ( );
En este momento el thread se hace activo. Esto no significa que se ejecute permanentemente en la máquina. En efecto, debe compartir uno o más procesos con los otros threads y los otros procesos del sistema. Pero forma parte de los procesos y threads que tienen cada uno regularmente un "lapso de tiempo". En la máquina, en el transcurso de un mismo segundo, el procesador ejecutará al proceso A durante 20 ms, después el B durante 20 ms…., después el primer thread del proceso J durante 20 ms, el segundo thread durante 20 ms, etc…, hasta volver al primer proceso. Sin embargo los lapsos de tiempo son en general muy breves, lo que hace que el usuario tenga la impresión de que prodesos y threads activos se ejecutan de forma permanente.

Por tanto, un thread activo es un thread al que el procesador asigna regularmente un lapso de tiempo. Desde el punto de vista del sistema, se distingue los procesos y threads activos en espera (de su lapso de tiempo) del o de los procesos o threads activos realmente en curso de ejecución. Desde el punto de vista del usuario, un thread activo es un thread en curso de ejecución

volver


THREAD DORMIDO

Un thread puede estar dormido. Esto significa que no consume recursos de máquina, pero que una vez despertado se hará activo. Varios casos pueden conducir a un thread al estado dormido ; a cada caso de adormecimiento corresponde una y sólo una solución para desvelarlo. Lo que simplifica su comprensión.

Imaginemos varios Príncipes encantados para despertar a la Bella durmiente del Bosque…

El primer caso es cuando el thread decide él mismo dormir un poco :

	class BABD extends Thread {  
		public void run ( ) {  
			System.out.println ("Tengo sue�o") ;  
			try {  
				sleep (1000 * 3600 *24 * 365 * 100) ;  
			}  
			catch (Exception e) { ; }  
			System.out.println ("He dormido 100 a�os") ;  
		}  
	}

Esta Bella durmiente del Bosque es desafortunadamente imposible de despertar. Hay que esperar que el tiempo pasado como parámetro al método sleep ( ) transcurra íntegramente.

Otra posibilidad para hacer pasar un thread del estado activo al estado dormido es llamar al método suspend( ). Como el método sleep ( ), el método suspend ( ) puede ser llamado desde el interior de la clase o bien desde el exterior. Veamos un ejemplo donde un thread es dormido y despertado desde el exterior :

	class BABD extends Thread {  
		public void run ( ) {  
			while (true) {  
				System.out.println ("BlaBlaBla") ;  
			}  
		}  
	}
	/* �. * /
	Hildegarda = new BABD ( ) ;  
	Hildegarda.start ( ) ;  
	System.out.println (" Hidelgarda") ;  
	System.out.println (" Tu madre y yo") ;  
	System.out.println (" Tenemos algo que decirte") ;  
	System.out.println (" HILDEGARDA, �QUIERES
	ESCUCHARME ?") ;  
	Hildegarda.suspend ( ) ;  
	System.out.println (" Hemos decidido casarte.") ;  
	System.out.println (" Espero que est�s
	contenta") ;  
	Hildegarda.resume ( ) ;

Este programa dará un resultado mezclando el BlaBlaBla producido por el thread Hildegarda con la salida del programa principal :

	BlaBlaBla  
	Hildegarda  
	BlaBlaBla  
	Tu madre y yo  
	BlaBlaBla  
	Tenemos algo que decirte  
	HILDEGARDA, �QUIERES ESCUCHARME ?  
	Hemos decidido casarte  
	Espero que est�s contenta  
	BlaBlaBla  
	BlaBlaBla

Un Thread puede estar dormido también cuando espera un recurso de un sistema de entrada/salida ; durante la apertura de un fichero el thread está en un estado dormido. Sólo sale de este estado cuando la operación de entrada/salida ha terminado (bien o mal). Esto ocurre de hecho muy a menudo, por ejemplo en las esperas de entrada por teclado. Un thread que incluya el código siguiente :

	
	System.in.read ( ) ;

está dormido mientras el usuario no haya pulsado un carácter en el teclado.

La última condición de inactividad tras el sueño, la suspensión y la espera de recurso : la espera basada en una condición.

volver


THREAD MUERTO

El último estado en el que un thread puede encontrarse es la muerte. Esta puede intervenir de dos formas diferentes :

En el primer caso, el thread se para porque ha terminado de hacer aquello para lo que había sido escrito. En el ejemplo de la Liebre y la Tortuga, cada uno de estos animales se para cuando ha recorrido la distancia prescrita. Los threads mueren al final de la carrera.

En el segundo caso, una intervención exterior al thread (en el programa principal) lo mata sin remisión. Volvamos al ejemplo de Hildegarda, y supngamos que su querido padre esté realmente harto de su parloteo :

	class BABD extends Thread {  
		public void run ( ) {  
			while (true) {  
				System.out.println ("BlaBlaBla") ;  
			}  
		}  
	}  
	 /* �. * /

	Hildegarda = new BABD ( ) ;  
	Hildegarda.start ( ) ;  
	System.out.println (" Hidelgarda") ;  
	System.out.println (" Tu madre y yo") ;  
	System.out.println (" Tenemos algo que decirte") ;  
	System.out.println (" HILDEGARDA, �QUIERES
	ESCUCHARME ?") ;  
	Hildegarda.stop ( ) ;  
	System.out.println (" Por fin la paz \n") ;

Este programa dará el resultado siguiente :

	BlaBlaBla  
	BlaBlaBla  
	Hildegarda  
	BlaBlaBla  
	Tu madre y yo  
	BlaBlaBla  
	Tenemos algo que decirte  
	HILDEGARDA, �QUIERES ESCUCHARME ?  
	Por fin la paz

volver


¿CÓMO CONOCER EL ESTADO DE UN THREAD ?

El método isAlive ( ) permite trazar la diferencia entre un thread vivo un thread muerto o en el limbo. No permite trazar la diferencia :

El método isAlive ( ) envía un booleano que es :



volver


PRIORIDADES

� PRIORIDADES ABSOLUTAS O RELATIVAS ?

Hemos explicado que un proceso se puede cortar en threads, que son otros tantos subprocesos. Sin embargo, algunos threads son más importantes que otros, o más consumidores de recursos que otros. Por ejemplo, el ejecutable Java tiene un thread funcionando como tarea de fondo que hace el trabajo de recogida de basura de la memoria. Es muy útil, pero si se activa permanentemente y el núcleo de Java le asigna tantos recursos como a mi programa principal, esto va a dividir el rendimiento de mi programa por dos.

Pero esto no es así. El núcleo ejecutable de Java implementa la noción de prioridad entre threads. Algunos tienen más medios que otros. Así el thread de recogida de basura tiene una prioridad más baja, lo que hace que trabaje sobre todo cuando los otros threads están dormidos, y especialmente cuando la aplicación espera entradas del usuario.

La prioridad es en principio una noción relativa. En la vida normal, una acción es más prioritaria que otra. Si fuera necesario clasificar todos los threads los unos en relación a los otros, esto sería exponencialmente largo y complicado. Cada creación de thread debería indicar cuál es su prioridad en relación a todos los otros ya creados y aún no muertos. Así, para simplificar el diseño de threads y la escritura del código, se indica la prioridad absoluta de un thread.

El reparto de tiempo entre dos threads de prioridades diferentes es simple : mientras el thread de mayor nivel no esté dormido, es él quien cuenta con los recursos. Si se duerme, da al otro thread la posibilidad de utilizar el recurso CPU, pero si se despierta, recupera el recurso.

El reparto de tiempo entre dos threads de igual prioridad depende de la máquina, esta funcionalidad no es portable.

volver


LOS LAPSOS DE TIEMPO

Es un método de reparto del recurso CPU. Si el núcleo que funiona es su maquina reparte el recurso CPU por lapsos de tiempo, significa que los threads de mayor prioridad tienen, cada uno a su vez, algunos milisegundos de tiempo de CPU. El núcleo devide cada segundo en lapos como rodajas de pan y reparte los lapsos entre los threads con la misma prioridad.

Si no es este el caso, un thread que haya acaparado el recurso no lo soltará hasta dormirse. El mismo programa no tendrá pues siempre el mismo comportamiento.

La solución a este prolema de portabilidad consiste en utilizar el método yield( ); éste permite a un thread compartir el recurso de CPU con sus hermanos, es decir los threads de igual prioridad. Por el contrario, no lo cederá a los threads de prioridad inferior.

Se presenta a continuación un ejemplo de tres threads que se reparten el tiempo; el programa muestra, para su información personal, los valores de prioridad máxima y mínima, tales como son definidas por le sistema. Son atributos finas static de la clase Thread. El programa hace girar seguidamente tres threads en función de los argumentos pasados en la línea de mandato.

	import java.io.*;
  
	class Prioridad {  
		static Animal laLiebre;  
		static Animal laTortuga;  
		static Animal elGuepardo;  
		public static void main ( String argv [ ] ) throws
		InterruptedException {  
			System.out.print ("La prioridad maxima es "
			+ Threads.MIN_PRIORITY);  
			System.out.println("y la minima es" + Threads.MAX_PRIORITY);  
			laLiebre = new Animal(Integer.value0f (argv[0].intValue ( ),
			 "L");  
			elGuepardo = new Animal(Integer.value0f(argv[1].intValue( ), "G");  
			laTortuga = new Animal(Integer.value0f(argv[2].intValue( ), "T");  
			laLiebre.start ( );  
			elGuepardo.star ( );  
			laTortuga.start ( );  
			laLiebre.join ( );  
			elGuepardo.join ( );  
			latortuga.join ( );  
			System.out.println ("");  
		}  
	}  

	class Animal extends Thread {  
		public Animal (int laPrioridad, String elNombre){  
			super (elNombre);  
			stePriority (laPrioridad);  
			System.out.print ("Prioridad de " + getName( ) + ": " + "laPrioridad + " ");  
		}  
		public void run ( ) {  
			for (int i = 0; i < 10; i++) {  
				System.out.print (getName ( )+ " ");  
				// yield ( );  
			}  
			System.out.println ("\n" + getName ( )+ "ha llegado");  
			return;  
		}  
	}
  

Observerá que yield ( ) está comentado, veremos pronto porqué. Si se hace funcionar este programa bajo Windows 95, se obtienen los resultados siguientes:

	c:\ ProgramaJava\Threads> java prioridad 2 2 2  
	La prioridad maxima es 10 y la minima es 1  
	Prioridad de L: 2 prioridad de G: 2 Prioridad de
	T: 2  
	L L L L L L L L L L  
	L ha llegado  
	G G G G G G G G G  
	G ha llegado  
	T T T T T T T T T  
	T ha llegado
	c:\ProgramaJava\Threads> java Prioridad 2 2 4  
	La prioridad maxima es 10 y la minima es 1  
	Prioridad de L: 2 Prioridad de G: 2 Prioridad de
	T: 4  
	T T T T T T T T T T  
	T ha llegado  
	L L L L L L L L L L  
	L ha llegado  
	G G G G G G G G G G  
	G ha llegado  

Cuando todos los Threads tienen la misma prioridad, el que toma el mando primero ( aquí es la Liebre el primer thread lanzado) no lo suelta hasta haber terminado su tarea (primer caso). En el segundo caso, un thread tiene una prioridad más importante que los otros (laTortuga), es él quien toma el mando y no lo suelta hasta haber finalizado su tarea.

Ahora, descomentamos el yield ( ) que figura en el ejemplo. Cada thread será menos egoista, y pasará el control al sistmea en las llamadas a yield ( ).

Obtenemos ahora los resultados siguientes:

	C:\ ProgramaJava> java Prioridad 2 2 2  
	La prioridad maxima es 10 y la minima es 1  
	Prioridad de L: 2 prioridad de G: 2 Prioridad de
	T: 2  
	G T L G T L G T L G T L G T L G T L G T L G T L G
	T L G T L  
	G ha llegado  
	L ha llegado  
	T ha llegado

Los tres threads con la misma prioridad, han trabajado a la misma velocidad y han llegado casi al mismo tiempo.

Por el contrario, si se dan prioridades diferentes, siempre será el primero el que consuma casi toda la carga de CPU, antes de pasar el relevo al siguiente, el cual le hace al tercero lo que no le ha gustado que le hicieran : hacerlo esperar.

	C:\ ProgramaJava> java Prioridad 2 3 4  
	La prioridad maxima es 10 y la minima es 1  
	Prioridad de L: 2 prioridad de G: 3 Prioridad de
	T: 4  
	T T T G T T T T T T T   
	T ha llegado  
	G G G L G G G G G G   
	G ha llegado  
	L L L L L L L L L  
	L ha llegado

Observe el ejemplo anterior con mayor atención : se observa que los threads de menor prioridad han sonseguido tomar una pequeña parte del tiempo de CPU antes de que el thread de mayor prioridad haya pasado el testigo : se encuentra una "G" entre las "T"

En conclusión, si threads de la misma prioridad debe compartir el recurso CPU, no hay que dudar en utilizar el método yield ( ) para asegurarse un reparto homogéneo de recursos entre estos diferentes threads.

volver


COMPARTIR DATOS

SAN CRONISMO

Una de las dificultades principales de utilización de los threads reside en el hecho de compartir los datos. Cuando se ejecuta un método , es posible que el thread actual pase el testigo en medio del método y otro thread ejecute el mismo método, modificando los valores locales.

Veamos un ejemplo ; en un banco, dos cajeros utilizan el mismo aparato para contar los billetes de banco de 5.000 ptas. Cada uno de ellos tiene 15.000 billetes por contar y señala en voz alta cuando ha llegado a la mitad de su paquete. Al final, anuncia el número de billetes y la suma correspondiente.

Se va a representar por dos threads Cajero, utilizando cada uno un Contador, que es su atributo, y que se les da en la inicialización. Este objeto cuenta los billetes, y cada vez, suma el valor del billete a la suma total.

Veamos pues nuestros dos Cajero :

	import java.io.* ;  
	import java.lang.* ;

 
	class Banco {  
		public static void main (String argv [ ] ) {  
			try {  
				Contador co1 = new Contador ( ) ;  
				Contador co2 = new Contador ( ) ;  
				Cajero c1 = new Cajero (co1) ;  
				Cajero c2 = new Cajero (co2) ;  
				c1.start ( ) ;  
				c2.start ( ) ;  
				c1.join ( ) ;  
				c2.join ( ) ;  
				}  
			catch (Exception e ) {  
				System.out.println (e.toString ( ) ) ;  
			}  
		}  
	}  

	class Contador {  
		int numBilletes = 0 ;  
		long suma = 0 ;  
		void cuenta ( ) {  
			for (numBilletes = 0 ; numBilletes < 15000 ;numBilletes++) {  
				if (numBilletes == 7500 ) {  
					System.out.println ("He llegado a la mitad") ;  
				}  
				suma += 5000 ;  
			}  
			System.out.println (Integer.toString (numBilletes)
			+ "billetes de 5000 PTA hacen" + Long.toString (suma)
			+ "pesetas") ;  
		}  
	}

	class Cajero extends Thread {  
		Contador c ;  
		Cajero (Contador elC) {  
			c = elC ;  
		};  
		public void run ( ) {  
			c.cuenta ( ) ;  
		}  
	}

El resultado es entonces :

	C :\ProgramaJava\Threads> java Banco  
	He llegado a la mitad  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 75000000 pesetas  
	15000 billetes de 5000 PTA hacen 75000000 pesetas

lo que no es sorprendente.

Ahora, supongamos que el banco decide hacer economías, que un contador esté estropeado, o bien que el director de la sucursal sea un sádico neurópata, en pocas palabras, supongamos que los dos cajeros se vean obligados a compartir el mismo contador. Se modificará la llamada a los constructores de los objetos Cajero en este sentido :

	Contador co1 = new Contador ( ) ;  
	Cajero c1 = new Cajero (co1) ;  
	Cajero c2 =new Cajero (co1) ;

y el resultado es entonces :

	C :\ProgramaJava\Threads> java Banco  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 71945000 pesetas  
	15001 billetes de 5000 PTA hacen 71945000 pesetas

Uno de los cajeros no se ha enterado de que había contado la mitad de los billetes, el otro ha contado un billete de más y ambos han cometido errores de cálculo. Se han mezclado los billetes al servirse de la misma máquina.

En realidad, en nuestro programa, los dos threads han accedido al mismo tiempo a los atributos del objeto Contador, cada uno entremezclando sus operaciones con el otro, yendo al desastre.

Por lo tanto, debemos impedir a ambos objetos que accedan a los datos al mismo tiempo. Esto se hace declarando el método synchronized. Esta palabra clave precisa que un solo thread tiene derecho a trabajar en este método a la vez.

Reemplazamos pues :

	void cuenta ( )

por

	synchronized void cuenta ( )

El resultado es ahora el siguiente :

	
	C :\ProgramaJava\Threads> java Banco  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 75000000 pesetas  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 150000000 pesetas

El segundo thread ha esperado a que el primero hubiera terminado para utilizar a su vez el método cuenta ( ). Queda sin embargo un error : no hemos inicializado al principio del bucle for el atributo suma, cuando sí hemos inicializado el atributo numBilletes.

Otra forma de bloquear así la ejecución del método es declarar que se sincroniza el objeto Contador.

Se reemplaza entonces :

	public void run ( ){  
		c.cuenta ( ) ;  
		// "c" es el atributo Contador de la clase Cajero  
	}  

por

	public void run ( ) {  
		synchronized ( c ) {  
			c.cuenta ( ) ;  
		}  
	}

y el resultado es el mismo que si se hubiera declarado synchronized el método cuenta :

	C :\ProgramaJava\Threads> java Banco  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 75000000 pesetas  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 150000000 pesetas

Esto nos permite volver sobre este punto : el acceso al objeto es lo que está bloqueado para los otros threads, y no la ejecución de un método. Si un método es synchronized y si se ejecuta sobre un objeto en un thread, otro thread no puede llamar a este método sobre este objeto, pero puede llamar a este método sobre otro objeto. Volvamos al ejemplo de los dos cajeros y volvamos a nuestra situación inicial en la que cada uno de ellos tiene su objeto Contador. Si declaramos synchronized el método cuenta ( ), esto no impide a los dos Cajero llamar al mismo tiempo, cada uno a su objeto. El resultado es en efecto el siguiente :

	C :\ProgramaJava\Threads> java Banco  
	He llegado a la mitad  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 75000000 pesetas  
	15000 billetes de 5000 PTA hacen 75000000 pesetas  

Los dos threads han ejecutado el mismo método synchronized al mismo tiempo, pero cada uno sobre un objeto diferente.

Se ha visto que synchronized impide a los threads ejecutar el mismo tiempo el mismo método sobre el mismo objeto, pero no el mismo método sobre dos objetos diferenets. Simétricamente, ¿nos impide synchronized utilizar dos métodos diferentes sobre el mismo objeto ? Para saberlo, se va a duplicar el método cuenta ( ) en un método cuenta2 ( ) ; vamos a compartir un objeto Contador entre los dos Cajero y actuar de modo que uno ejecute cuenta ( ) y el otro cuenta2 ( ).

El resultado es el siguiente :

	
	He llegado a la mitad  
	He llegado a la mitad  
	15000 billetes de 5000 PTA hacen 75000000 pesetas (cuenta)  
	15000 billetes de 5000 PTA hacen 75000000 pesetas (cuenta2)

Los dos métodos se han desarrollado al mismo tiempo. En conclusión, ¿qué puede decirse de synchronized nombreFuncion o de su hermano pequeño synchronized (nombreObjeto) ? Impiden a dos threads que ejecuten la misma porción de código al mismo tiempo sobre el mismo objeto. Por el contrario, estos dos threads pueden ejecutar dos métodos diferentes sobre el mismo objeto, o ejecutar el mismo método sobre dos objetos diferentes.

Los métodos synchronized pueden fácilmente conducir a interbloqueos o a degradaciones de rendimiento. Es pues preferible utilizar esta palabra clave sólo para métodos cortos.

volver


ENCUENTRO

Los threads pueden organizar encuentros entre ellos, compartiendo une espera sobre un objeto común. Uno o más threads esperan que pase alguna cosa sobre un objeto y otro thread pone fin a su espera.

Esto es posible gracias a dos métodos de la clase Object : notify ( ) y wait ( ). Wait ( ), llamado en un thread, significa que el thread va a esperar que otro thread llame a notify ( ) en otro método del mismo objeto.

Se verá a continuación un ejemplo. En una clase, un alumno espera la distribución de las notas para una redacción. Su espera termina cuando el profesor da las notas. Los dos threads Alumno y Profesor sompartirán pues un objeto común : las notas.

El programa es el siguiente :

	import java.io.* ;  
	import java.io.lang.* ;
	
	class escuela {  
		public static void main (String argv [ ] ) {  
			try {  
				Notas ayAy = new Notas ( ) ;  
				Profesor p = new Prefesor (ayAy) ;  
				Alumno sufridor = new Alumno (ayAy, "sufridor") ;  
				sufridor.start ( ) ;  
				p.start ( ) ;  
				sufridor.join ( ) ;  
				p.join ( ) ;  
			}  
			catch (Exception e ) {  
				System.out.println (e.toString ( ) ) ;
			}  
		}  
	}
  
	class Notas {  
		synchronized void seHacenEsperar ( ) {  
			try {  
				wait ( ) ;  
			}  
			catch (InterruptedException e) {  
				System.out.println (e.toString ( ) ) ;  
			}  
		}  
		synchronized void serDifundidas ( ) {  
			notify ( ) ;  
		}  
	}
  
	class Alumno extends Thread {  
		Notas Notas ;  
		String miNombre ;
		Alumno (Notas lasNotas, String elNombre) {  
			Notas = lasNotas ;
			miNombre = elNombre;  
		}
  		public void run ( ) {  
			System.out.println (miNombre + "espera su nota");  
			Notas.seHacenEsperar ( );  
			System.out.println (miNombre + "ha recibido su nota") ;  
		}
	}
  
	class Profesor extends Thread {  
		Notas Notas ;  
		Profesor (Notas lasNotas) {  
			Notas = lasNotas ;  
		}
		public synchronized void run ( ) {  
			System.out.println ("VOY A DAR LAS NOTAS") ;  
			Notas.serDifundidas ( ) ;  
		 
	}

El resultado es ahora el siguiente :

	C :\ProgramaJava\Threads> java escuela  
	sufridor espera su nota  
	VOY A DAR LAS NOTAS  
	sufridor ha recibido su nota

En el thread Alumno, se ha llamado al método seHacenEsperar ( ) sobre las notas, método que se ha parado sobre el wait ( ) y no ha podido seguir hasta que otro thread ha llamado a serDifundidas ( ) sobre el mismo objeto.

Si los dos objetos utilizan este mecanismo sobre dos (paquetes de) Notas difententes, la espera del Alumno no termina jamás. Se modificará el inicio del program y se reemplazará :

	Notas ayAy = new Notas ( ) ;  
	Profesor p = new Prefesor (ayAy) ; 
	Alumno sufridor = new Alumno (ayAy, "sufridor") ;

por

	Notas ayAy = new Notas ( ) ;  
	Profesor p = new Prefesor (ayAy);  
	Alumno sufridor = new Alumno (new Notas ( ) , "sufridor") ;

El resultado es horrible, quedamos obligados a matar al alumno para abreviar su espera :

	C :\ProgramaJava\Threads >java escuela  
	sufridor espera su nota  
	VOY A DAR LAS NOTAS  
	^C

Se ha comprendido cómo funciona notify ( ). Pero, de hecho, ¿este mecanismo funciona sobre varios threads a la vez ?. No. Si ponemos a varios alumnos en espera se sus notas :

	Notas ayAy = new Notas ( ) ;  
	Profesor p = new Prefesor (ayAy) ;  
	Alumno nicolas = new Alumno (ayAy, "nicolas") ;  
	Alumno sufridor = new Alumno (new Notas ( ) , "sufridor") ;  
	nicolas.start ( ) ;  
	sufridor.star ( ) ;  
	p.start ( ) ;

uno de ellos no parece enterarse de que las notas han sido distribuidas :

	C :\ProgramaJava\Threads> java escuela 
	nicolas espera su nota  
	sufridor espera su nota  
	VOY A DAR LAS NOTAS  
	nicolas ha recibido su nota  
	^C

Hay que utilizar notifyAll ( ) y no notify ( ). El resultado es entonces satisfactorio, sea cual sea el número de alumnos :

	C :\ProgramaJava\Threads> java escuela  
	nicolas espera su nota  
	alcestes espera su nota  
	jorge espera su nota  
	sufridor espera su nota  
	VOY A DAR LAS NOTAS  
	nicolas ha recibido su nota  
	jorge ha recibido su nota  
	sufridor ha recibido su nota  
	alcestes ha recibido su nota

El orden en el que los threads reaccionan al notifyAll ( ) es aleatorio. Esto explica que el orden en el que los alumnos han recibido su nota no es el mismo que el orden en elque declaran esperar su nota.

wait ( ) tiene variantes que le permiten esperar con un timeout. Esto significa que wait ( ) pasa el testigo cuando el método notify ( ) ha sido llamado, pero también si este método no ha sido llamado y ha pasado cierto tiempo.

volver


¿CUÁNDO HAY QUE UTILIZAR QUÉ ?

Parece en efecto que los dos mecanismos, synchronized por una parte, y notify/wait por otra, se parecen en gran medida. Por tanto se puede pensar que se puede utilizar indiferentemente uno u otro.

En realidad no tienen objetivos diferentes :

El principal uso de este último mecanismo es intercambiar informaciones entre threads. Uno de los threads espera una actualización de un atributo. El otro actualiza el atributo y lo avisa.

volver


LOS THREADS DEMONIOS

Estos son en general threads de baja prioridad que giran en un bucle infinito, esperando que se les solicite que presten un servicio : limpiar la memoria, mostrar imágenes, etc.

Un thread se declara como demonio por la llamada a :

	setDaemon ( )

y se puede saber si un thread es un thread demonio haciéndole la pregunta :

	isDaemon ( )

Desde el punto de vista del sistema, lo que diferencia un thread demonio de los otros threads, es que el núcleo Java no espera su muerte para parar el programa. Los threads normales impiden al núcleo Java parar el programa principal, hasta que no mueren por suicidio ( fin del método run ( ) ), o por asesinato (llamada a stop ( ) ). En el caso de los threads demonios, el núcleo Java decide matarlos él mismo, si constata que sólo quedan threads demonios.

La gran ventaja de esta funcionalidad es la no necesidad de gestionar el fin de estos threads demonios en el código. Esto evita multiplicar las secuencias :

	threadImpresion.stop ( ) ;  
	threadPaginacion.stop ( ) ;  
	threadGestionMemoria.stop ( ) 
;

volver


LOS GRUPOS DE THREADS

En su creación, cada thread pertenece a un grupo de threads. Un grupo de threads se define por un objeto de la clase ThreadGroup. Existe al menos un grupo de threads en todo programa Java, es el grupo main, creado por el núcleo. En la creación de un thread, puede especificar o no el grupo al que pertenece ; si no lo especifica, pertenecen de modo predeterminado al mismo grupo que su padre (el thread en el que ha sido creado el thread actual). Si no precisa jamás un grupo de thread, pertenecerán todos al grupo main.

Para especificar el grupo de threads al que pertenece un thread, se pasa al grupo como parámetro del constructor del thread :

	ThreadGroup losAnimales = new ThreadGroup ("Animales") ;  
	Thread Liebre = new Thread (losAnimales, "Liebre") ;  
	Thread Conejo = new Thread (losAnimales, "Conejo") ;  
	Thread Hipocampo = new Thread (losAnimales, "Hipo") ;

Los grupos de threads forman una jerarquía porque, cuando crea un grupo de thread, lo hace en un thread que es eventualmente el programa principal. Cuando se crea un grupo de threads, es hijo del grupo de threads actual. Es decir : si en el thread Conejo perteneciente al grupo de threads Animales, creamos un nuevo grupo de threads OrganosDelConejo, est último es hijo de Animales. En la práctica, todos nuestros grupos de threads serán hijos del grupo main.

Esta noción de jerarquía tiene su importancia, porque algunas propiedades son hereditarias y algunas acciones efectuadas los son implícitamente sobre los hijos.

Entre las acciones que se pueden efectuar sobre los grupos, algunas tienen un efecto sobre los threads pertenecientes al grupo y otras sobre los threads que se crearán en este grupo.

Así los métodos:

	setDaemon ( )  
	setMaxPriority ( )

no modifican los threads del grupo ya existentes, mientras que los métodos :

	resume ( )  
	stop ( )  
	suspend ( )

actúan sobre todos los threads del grupo y de sus grupos descendientes.

Los métodos:

	getMaxPriority ( )  
	getDaemon ( )  
	getName ( )  
	getParent ( )

permiten obtener los valores de los atributos que describen el grupo de threads.

El interés principal de los ThreadGroup es poder efectuar una acción sobre un conjunto de threads a la vez, sin tener que llamar a un método sobre cada uno de ellos. Por ejemplo, si gestionamos una interfaz X25 y creamos tantos threads como vías hay de comunicación posibles, una sola llamada a un método de ThreadGroup para, retoma, o cierra todos los threads de un solo golpe. Sin el mecanismo de los ThreadGroup, sería necesario mantener una tabla de todas las referencias de los threads sobre los que se quiere actuar. Con el ThreadGroup, basta con conservar una referencia, la del grupo, y se actúa sobre un conjunto de threads en una sola vez.

volver


DIFICULTADES DE IMPLEMENTACIÓN

Se empezará primero por recordar el interés del lenguaje Java par la utilización de los threads : una vez más, es la portabilidad. Sin embargo se ha visto que los threads no eran totalmente portables de un sistema a otro en Java. Pero si utiliza un lenguaje de desarrollo clásico, constatará que los threads :

De un sistema a otro, si no utiliza Java, no son portables en absoluto, y las diferencias de implementación entre sistemas pueden conducir a una reescritura completa del código en una migración.

Por ejemplo, el lenguaje C++ no ofrece ninguna facilidad particular para los threads. La utilización de estos en C++ pasa pues por la llamada a funciones de sistema como en C.

Sin embargo Java no le ahorra todas las dificultades relacionadas con la utilización de los threads. Los problemas que se encuentran cuando se utilizan los threads en Java para programas de tamaño respetable son los siguientes :

Para demostrar este último punto, se examinará el ejemplo siguiente (inestable.java)

	import java.io.* ;
  
	class CompartirString {  
		public static void main (String argv[ ] ) {  
			Buffer = new StringBuffer (" ") ;  
			L =new RellenoDeString ("L", Buffer) ;  
			L.start ( ) ;  
			T = new RellenoDeString ("T", Buffer) ;  
			T.start ( ) ;  
			for (int i = 1 ; i < 1000000 ; i++) { ; }  
			top ( ) ;   
			top ( ) ;  
			int numL = 0 ;  
			int numT = 0 ;  
			for (int i = 0 ; i <Buffer.length ( ) ;i++ ) {  
				if (Buffer.charAt (i ) == "L" numL++;
				else numT++ ;  
			} ;  
			System.out.printl("num de L : " +numL + ", num de T : " + numT) ;
		}  
		static StingBuffer Buffer ;  
		static RellenoDeString L ;
		static RellenoDeString T ;
	}  

	class RellenoDeString extends Thread {  
		char miMarca ;  
		StringBuffer miBuffer ;  
		public RellenoDeString (char Lamarca, StringBuffer ElBuffer) {  
			miMarca = LaMarca ;  
			miBuffer = ElBuffer ;  
		}  
		public void run ( ) {  
			while (true) {  
				miBuffer.append (miMarca) ;  
			}  
		}  
	}

En este ejemplo dos threads rellenan continuamente un buffer con una letra que les es propia hasta que el programa principal los mata. Tras haber cumplido tranquilamente con su tarea el programa principal cuenta el número de letras que cada uno de los desafortunados threads ha escrito en el buffer. De una ejecución a otra el rellenado del buffer no será el mismo. En un Pentium P90, este mismo programa ejecutado tres veces ha dado los resultados siguientes :

	C :\ ProgramaJava\Threads>java CompartirString  
	num de L : 4537, num de T : 4318  
	C :\ ProgramaJava\Threads>java CompartirString  
	num de L : 3950, num de T : 3727  
	C :\ ProgramaJava\Threads>java CompartirString  
	num de L : 4896, num de T : 3598

No sólo varía el número de T y de L cada vez, sino que además la relación entre ambas también varía.

Así la utilización de los threads debe acompañarse de las recomendaciones siguienes :



volver


CONCLUSIÓN

Los threads son :



volver


Hosted by www.Geocities.ws

1