La liebre y la tortuga (y el guepardo)

Java dispone de un mecanismo de prioridades para los threads,de modo de poder asignar más tiempo de CPU a un threadque a otro. Típicamente se asigna una prioridad de 1 a10 (10 es la mayor prioridad) mediante setPriority, comoen el ejemplo que sigue:

public class Ejemplo21 {    static Animal	tortuga;    static Animal	liebre;    static Animal	guepardo;    public static void main(String argv[])      throws InterruptedException {	tortuga  = new Animal(2, "T");	liebre   = new Animal(3, "L");	guepardo = new Animal(4, "G");	tortuga.start();	liebre.start();	guepardo.start();	tortuga.join();	liebre.join();	guepardo.join();    }}class Animal extends Thread {	String nombre;	public Animal(int prioridad, String nombre) {		this.nombre = nombre;		setPriority(prioridad);	}	public void run() {		for (int x = 0; x < 30; x++) {			System.out.print( nombre );			yield();		}		System.out.println("\nLlega "+nombre );	}}

La salida de este programa, ejecutado con java Ejemplo21,es por ejemplo:

C:\java\curso>java Ejemplo21GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGLlega GLTLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLlega LTTTTTTTTTTTTTTTTTTTTTTTTTTTTTLlega T

Como se ve, a pesar de haber arrancado antes la tortuga, casitodo el tiempo de CPU lo usa primero el Guepardo, luego la Liebre(aunque algo queda para la pobre tortuga, como se ve en la T marcada),y finalmente para la Tortuga. No todas las corridas ni todos lossistemas dan igual salida, ya que ésta depende de la cargadel procesador y de la implementación de Java particular.

Este programa simplemente crea tres animales (clase Animal),asigna un thread a cada uno y los ejecuta. Este ejemplo estáhecho en base a uno del libro "Programación Java"de Macary y Nicolas.

Sincronicemos los relojes

Un problema básico del multithreading es cuando variosprogramas (o, para el caso, varios threads) acceden a los mismosdatos: ¿cómo sabemos si uno de ellos no los modificamientras los está usando otro?.

Veamos un ejemplo, donde suponemos que varios threads usan lavariable valorImportante:

	if  (valorImportante > 0 ) {		..... algo se procesa acá ........		valorImportante = valorImportante - 1;		..... sigue.....................	}

¿Cómo nos aseguramos que valorImportanteno cambió entre el if y la línearesaltada? Otros threads pueden haberlo modificado mientras tanto.Asimismo, puede suceder que dos threads estén ejecutandola misma porción de código, y se pierda uno de losdecrementos. Imaginen algo así:

	(antes)		valorImportante = 10	(thread 1)	lee valorImportante = 10	(thread 2)	lee valorImportante = 10	(thread 1)	10 -1 = 9	(thread 2)	10 -1 = 9	(thread 2)	asigna 9 a valorImportante	(thread 1)	asigna 9 a valorImportante	(después)	valorImportante = 9

Como vemos, a pesar de haber restado dos veces, hemos perdidouna de las restas. Aunque usemos -= en vez de la resta es lo mismo,porque el código igualmente se resuelve en varios pasos(varias operaciones atómicas).

Para evitar esto, Java nos brinda la palabra clave Synchronized,que bloquea el acceso a una variable a todos los threads menosel que lo está usando.

Vamos a ver un caso específico; se trata de dos contadoresque usan el mismo sumador para sumar de a uno una cantidad a.Supuestamente entre los dos deben llevar el sumador (a)hasta 20000.

// Archivo Ejemplo22.java, compilar con javac Ejemplo22.java, ejecutar con java Ejemplo22public class Ejemplo22 {	public static void main(String argv[]) {		Sumador A = new Sumador();		// un único sumador		Contador C1 = new Contador(A);	// dos threads que lo usan...		Contador C2 = new Contador(A);	// ...para sumar		C1.start();		C2.start();		try {			C1.join();			C2.join();		}		catch (Exception e) {			System.out.println(e);		}	}}class Contador extends Thread {	Sumador s;	Contador (Sumador sumador) {		s = sumador;		// le asigno un sumador a usar	}	public void run() {		s.sumar();		// ejecuto la suma	}}class Sumador {	int a = 0;	public void sumar() {		for (int i=0; i<10000; i++ ) {			if ( (i % 5000) == 0 ) {		// "%" da el resto de la división:				System.out.println(a);	// imprimo cada 5000			}			a += 1;		}		System.out.println(a);			// imprimo el final	}}

Ejecutando esto nos da más o menos así (cada corridaes diferente, dependiendo de cómo se "chocan"los threads y la carga de la CPU):

C:\java\curso>java Ejemplo220878926104341415917855

Esto se debe justamente a lo que explicábamos al principio:a veces los dos threads intentan ejecutar a+= 1 simultáneamente, con lo que algunos incrementosse pierden.

Podemos solucionar esto modificando el método run():

	public void run() {		synchronized (s) {			s.sumar();		}	}

Con esto, sólo a uno de los dos threads se les permiteejecutar s.sumar() por vez, y se evita el problema. Por supuesto,el otro thread queda esperando, por lo que más vale noutilizar esto con métodos muy largos ya que el programase puede poner lento o aún bloquearse.

La salida ahora será:

C:\java\curso>java Ejemplo220				<5000				< primer thread10000				<10000				(15000				( segundo thread20000				(

Lo mismo logramos (y en forma más correcta) declarandocomo synchronized al método sumar():

	public synchronized void sumar() { .............

Esto es mejor porque la clase que llama a sumar()no necesita saber que tiene que sincronizar el objeto antes dellamar al método, y si otros objetos (en otros threads)lo llaman, no necesitamos preocuparnos.

Más sincronización

Otra manera de sincronizar el acceso de los threads a los métodos,es lograr que éstos se pongan de acuerdo entre sí,esperando uno hasta que otro realizó alguna tarea dada.Para esto se usan los métodos wait()y notify(). Cuando un thread llama await() en un método de un objetodado, queda detenido hasta que otro thread llame a notify()en algún método del mismo objeto.

Por ejemplo, vamos a suponer cuatro empleados que se encuentrancon su jefe y lo saludan, pero sólo luego de que éstelos salude primero.

public class Ejemplo23 {	public static void main(String argv[]) {		Saludo hola = new Saludo();		Personal pablo = new Personal(hola, "Pablo", false);		Personal luis = new Personal(hola, "Luis", false);		Personal andrea = new Personal(hola, "Andrea", false);		Personal pedro = new Personal(hola, "Pedro", false);		Personal jefe = new Personal(hola, "JEFE", true);		pablo.start();		luis.start();		andrea.start();		pedro.start();		jefe.start();		try {			pablo.join();			luis.join();			andrea.join();			pedro.join();			jefe.join();		}		catch (Exception e) {			System.out.println(e);		}	}}class Saludo {	synchronized void esperarJefe(String empleado) {	  try {		wait();		System.out.println(empleado+"> Buenos dias jefe!");	  }	  catch (InterruptedException e) {		System.out.println(e.toString());			  }	}	synchronized void saludoJefe() {		System.out.println("JEFE> Buenos dias!");		notifyAll();	}}class Personal extends Thread {	String nombre;	Saludo saludo;	boolean esJefe;	Personal (Saludo s, String n, boolean j) {		nombre = n;		saludo = s;		esJefe = j;	}	public void run() {		System.out.println("("+nombre+" llega)");		if (esJefe)			saludo.saludoJefe();		else			saludo.esperarJefe(nombre);	}}

Usé notifyAll() en lugar de notify(),porque en el segundo caso sólo se notificaría alprimer thread (el primer empleado en llegar) y no a los demás,que se quedarían en el wait().

Como se ve en la salida, a pesar de que los empleados estánen condiciones de saludar, no lo hacen hasta que no llega el jefe:

C:\java\curso>java Ejemplo23(Pablo llega)(Luis llega)(Andrea llega)(Pedro llega)(JEFE llega)JEFE> Buenos dias!Luis> Buenos dias jefe!Pedro> Buenos dias jefe!Andrea> Buenos dias jefe!Pablo> Buenos dias jefe!

Aquí hice trampa: a veces, el jefe llega y saluda antesque alguno de los empleados, por lo que ese empleado se quedaesperando indefinidamente. Prueben de modificar las clases paraque el jefe no salude hasta que no estén todos los empleadospresentes...

Un buen ejercicio antes del próximo capítulo!

Solucion al problema propuesto

<> Página de tutoriales <> Página Anterior <> Siguiente Capitulo <>