Sólo Programadores Banner 468x60
Este mes en Sólo Programadores
Contenido del CD-ROM
Índice temático
Suscripción

JAVA

Concurrencia y sincronización de threads (II)

A lo largo de este artículo vamos a tratar sobre cómo los threads pueden compartir datos de una forma correcta, sin problemas, utilizando los diferentes métodos disponibles para ello. Además se detallarán los modos para la comunicación e intercambio de datos entre éstos.

La utilización de threads que comparten datos puede dar a lugar a situaciones anómalas cuya solución exige el uso de un mecanismo de sincronización de acceso al recurso compartido. El lenguaje Java ofrece unos sencillos mecanismos para solucionar este problema, por lo que la solución suele resultar por regla general bastante simple y efectiva.

SINCRONIZACIÓN Y COMUNICACIÓN entre threads

Estos mecanismos se basan en el concepto de "monitores", un esquema ampliamente utilizado y que fue desarrollado por C.A.R. Hoare. No es necesario tener una comprensión exacta de este mecanismo, pero es de gran ayuda para conseguir un entendimiento de los problemas que se abordarán.

Un monitor es esencialmente un cerrojo, que se aplica a un determinado recurso que es compartido, de forma que a este recurso pueden acceder múltiples threads, pero únicamente por uno de ellos a la vez.

El mecanismo de cerrojos se puede entender con un ejemplo de la realidad. Supongamos que tenemos un hotel con una única habitación libre. Cuando la ocupe un inquilino, a este inquilino se le da una llave que le permite disfrutar de esa habitación, pero en cuanto se marcha y devuelve la llave, esa habitación esta disponible para otros posibles inquilinos, de manera que la posesión de la llave determina quién esta usando el recurso, o sea, la habitación.

El problema clásico que determina el uso de cerrojos es el siguiente. Supongamos que dos thread se alternan en el incremento del contenido, de una variable común, un contador. En un determinado momento, el thread A lee el contenido del contador, lo incrementa en uno y va a guardar en la variable el nuevo contenido. Se puede dar el caso de que en ese momento el sistema pase al thread de activo a dormido (se le acabó su tiempo), y no ha podido guardar el valor correcto del contador.

Ahora le toca el turno al thread B, que verá el mismo contenido que el thread anterior, debido a que éste no le dio tiempo a guardar el resultado correcto. A continuación thread el actual incrementará el contador y guardará su resultado. Ahora se le acaba el tiempo, el sistema ejecuta el thread A que en vez de incrementar el valor según lo dejó el thread B escribirá su valor calculado anteriormente y continuará donde terminó su ejecución, escribiendo su propio resultado, por lo que tenemos que los dos thread en vez de incrementar el contenido de una variable en dos unidades lo han hecho en uno.

El uso de recursos compartidos usando threads puede dar lugar a situaciones anómalas

Afortunadamente Java se encarga de todo el proceso de sincronización, ejecuta internamente todo el mecanismo de acceso, manipula el estado del cerrojo, o sea, su bloqueo y correcto desbloqueo, por lo que las aplicaciones sólo tienen que indicar los recursos que se van a bloquear.

LA PALABRA CLAVE: SYNCHRONIZED

Por regla general se suele necesitar sincronización entre threads cuando queremos acceder ordenadamente a un recurso compartido, normalmente un objeto. En otras palabras, la sincronización se encarga de asegurar que un thread en un determinado momento pueda manipular un objeto. En Java, cada objeto tiene un cerrojo asociado con él. Para ser más específicos, cada clase y cada instancia de la clase tiene su propio cerrojo. Gracias a ello, se puede utilizar la palabra clave synchronized que se utiliza para definir qué parte de código (en realidad un método completo) puede ser accedido por un único thread, por lo que sé encarga de gestionar el mecanismo de cerrojos.

UN EJEMPLO: EL TOTAL BANCARIO

Desarrollemos un ejemplo que ponga en práctica la teoría. Supongamos un banco que consta de dos cajeros que se encargan de contar billetes de un valor cualquiera en pesetas. Cada uno de ellos tiene que contar una cantidad de billetes y cuando ha finalizado su tarea muestra el número total de billetes que ha contado y la suma correspondiente. Si hacemos que los dos cajeros sean dos threads y que cuenten el número de billetes y den la suma de los que han contado, podremos poner en práctica la sincronización.

En el primer caso vamos a hacer que cada cajero cuente su propio montón de billetes, para mostrar los cambios y cómo es posible el cambio de los resultados:

import java.io.*;

     import java.lang.*;


     class Banco {

       public static void main ( String args[])
       {
   		  try {

           // Declaramos los dos montones 
              de billetes

			                 Contador co1 = new Contador ();

                 			Contador co2 = new Contador ();

                 			// Declaramos los dos cajeros

                 			Cajero c1 = new Cajero(co1);

                 			Cajero c2 = new Cajero(co2);


                 			// Se ponen a contar..

                 			c1.start();

                 			c2.start();			

                 			c1.join();

                 			c2.join();

              		}

              		catch ( Exception e ){
 
                     			e.printStackTrace();	

              		}

	        }

   }

La clase Contador se encargará de contar los billetes y almacenar la suma y el total de billetes contados. En este caso concreto se supondrá que el montón es de billetes de 2.000 pesetas.

class Contador {

        	int numBilletes = 0 ;

        	long suma = 0 ;

        	final int TOTAL_BILLETES = 10000 ;

        	final int VALOR_BILLETES = 2000 ;

        	void cuenta () {

           		// A contar la suma de los billetes 

           		for ( numBilletes =0 ; numBilletes 
             < TOTAL_BILLETES; numBilletes ++ )

      {

             			suma += VALOR_BILLETES ;  
                // Billetes de 2000 pts

             			Thread.yield();

              		}

		              System.out.println 
                ( numBilletes+ " suman : "+
                suma + " pts");

         	}

      }

En la siguiente clase declaramos los thread que serán los cajeros que cuentan. Cada cajero recibe un montón para contar, o sea, un objeto de tipo Contador:

class Cajero extends Thread {


	          Contador contadorCajero ;

          	Cajero ( Contador paramContador ) {

		         contadorCajero = paramContador ;

          	}

		 

          	public void run () {

         		contadorCajero.cuenta();

          	}

     }

Se obtiene lo siguiente:

10000 suman: 20000000 pts

10000 suman: 20000000 pts

Es un resultado correcto, ya que cada thread tenía un montón de billetes diferentes para contar.

Ahora supongamos que los dos cajeros deben contar del mismo montón, o sea, lo comparten y por tanto, la suma de lo que haya contado cada uno debe ser el resultado total.

Para ello, modificaremos el código añadiendo lo siguiente

// Declaramos los dos montones de billetes

     Contador co1 = new Contador ();

     // Ahora sobra, Contador co2 = new Contador ();
     // Declaramos los dos cajeros y el mismo montón.

     Cajero c1 = new Cajero(co1);

     Cajero c2 = new Cajero(co1);

Con este cambio obtenemos:

10000 suman: 20002000 pts

10001 suman: 20002000 pts

Observando el resultado, comprobamos que los dos cajeros no saben contar y además, un cajero ha contado un billete más que el otro, y encima el total sumado es el mismo en ambos casos, por lo que está claro que ha producido un error de sincronismo. En un momento un thread ha interrumpido a otro en una parte crítica, en la suma o en la cuenta de los billetes.

Un cerrojo es un mecanismo que aplicado sobre un recurso permite un acceso correcto a éste por parte de los threads

Por tanto, debemos utilizar un mecanismo de sincronización que garantice que cuando un cajero cuente un billete y lo sume, el otro no pueda intentar coger el mismo billete y sumarlo. La solución que ofrece Java para resolver este problema es de lo más simple y eficiente, utilizando la cláusula synchronized en la declaración del método donde se realiza la tarea "crítica".

Por tanto, cambiaremos el método void cuenta() por:

synchronized void cuenta ()

Si realizamos ese cambio, obtenemos el siguiente resultado:

10000 suman : 20000000 pts

10000 suman : 40000000 pts

Es el resultado esperado. Una vez que el primer cajero entra, no acaba hasta que termina de contar, debido a que mientras no finalice su trabajo no ha salido del método, impidiendo que el otro cajero pudiera tocar el montón de billetes para contar, porque hemos declarado el método cuenta() con la cláusula synchronized.

De todos modos, hay un detalle que se tiene que explicar. ¿Por qué el cajero que finaliza cuenta 40000000 en vez de 20000000?. Esto ocurre porque no se inicializa la variable suma antes del bucle que cuenta los billetes, por lo que el segundo cajero continúa la suma en donde la dejó el anterior.

Si modificamos el código e incluimos la inicialización, tendremos:

void cuenta () {

     // Cada cajero cuenta lo suyo …

    	     suma = 0 ;

         	// A contar la suma de los billetes

    	     for ( numBilletes =0 ; numBilletes 
          < TOTAL_BILLETES  ;numBilletes ++ ) {

                      suma += VALOR_BILLETES ;
           Thread.yield();

		                 }

A partir de este momento obtenemos el siguiente resultado esperado tal y como detallamos:

10000 suman : 20000000 pts

10000 suman : 20000000 pts

Otra forma de realizar la sincronización consiste en declarar el objeto compartido como sincronizado en vez del método que lo contiene. Se realiza entonces el siguiente cambio:

public void run(){

		          contadorCajero.cuenta();

	       } 

por:

public void run(){

		        synchronized (contadorCajero ) {

           			contadorCajero.cuenta();

	       	 }

 	   }

Se ejecuta esta nueva modificación y se obtiene el mismo resultado. En este caso lo que se está sincronizando es el acceso al objeto, no el método que lo contiene.

El uso de cerrojos permite la comunicación entre threads de forma que uno puede bloquear a otro debido a diferentes causas, como una espera, un cálculo, etc

La cuestión está en que si un método es de tipo synchronized y si se ejecuta sobre un objeto en un thread, otro thread no puede llamar a este método sobre ese objeto, pero puede llamar a otro método sobre otro objeto.

Recordando el caso en que el método estaba declarado con synchronized, esto nos garantizaba que dos thread no podían ejecutar a la vez ninguna parte del método, pero sobre el mismo objeto, no sobre diferentes objetos. Si declaramos dos objetos diferentes podríamos tener los mismos problemas.

Además, la cláusula synchronized permite ejecutar dos métodos diferentes sobre el mismo objeto o el mismo método sobre dos objetos diferentes, pero impide que más de un thread ejecute la misma porción de código al mismo tiempo sobre el mismo objeto.

La cláusula synchronized permite sincronizar el acceso a un determinado método

Llegados a este punto debemos de destacar que el hecho de usar synchronized de una forma poco práctica puede conducirnos a interbloqueos y provoca una degradación del rendimiento del código, cerca de cuatro veces, por lo que es preferible utilizar esta palabra clave sólo para métodos cortos y en aquellos que se consideran como imprescindibles.

UN POCO MÁS ALLÁ. LOS MÉTODOS WAIT() Y NOTIFY()

Si bien mediante la utilización de la cláusula synchronized podemos tener un acceso sincronizado a un método o un objeto de un bloque de código, el uso de wait() y notify() permite ampliar esta posibilidad. Estos métodos pertenecen de por sí a la clase Object. Cada objeto en Java es una subclase de Object, por lo que gracias a la herencia podemos utilizar sus métodos. Usando wait() y notify() un thread puede detenerse sobre un cerrojo en un punto cualquiera que podemos definir, y entonces puede esperar a que otro thread le notifique que puede continuar. Todo esto ocurre dentro de bloques sincronizados y se cumple que un único thread puede ejecutarse a la vez. La ventaja de este método es que ofrece mayor flexibilidad de bloqueo y una forma de controlar los thread de manera más práctica.

La ejecución del método wait() en un bloque sincronizado determinado permite que el thread quede en un modo de "espera o bloqueado" sobre un determinado cerrojo y por tanto pasa al estado de "dormido". Un thread puede necesitar ser bloqueado porque tiene que esperar a que otro thread haya terminado de realizar una tarea en otra parte del programa, o se cumpla una determinada condición, etc. Entonces, cuando se cumpla dicha condición o la tarea que espera se haya realizado, el thread que estaba en "espera" por el bloqueo es "desbloqueado" mediante una llamada al método notify() por el thread que estaba activo, y el que estaba dormido pasará a estado de activo, tomando el control sobre el cerrojo correspondiente.

Cuando el thread que quedó bloqueado toma el control sobre el cerrojo prosigue con la siguiente sentencia donde se quedó, esto es, la siguiente sentencia tras wait(). De todos modos, hay que tener en cuenta que esto depende de cuando el thread que libera el cerrojo por el uso del método notity() ya que si no se realiza la llamada el thread bloqueado no continuaría ejecutándose. Por ello, existe una versión sobrecargada del método wait() que permite especificar un determinado tiempo de espera, a partir del cual el thread que está bloqueado despierta. Tiene la ventaja de que se evitan bloqueos extraños pero no garantiza un uso correcto de los cerrojos.

Vamos a desarrollar un ejemplo sencillo en el que se utilicen wait y notify para poder apreciar la utilidad de estos métodos. Supongamos que tenemos unos alumnos de una clase esperando a que el profesor les comunique sus notas. Los dos threads de Alumnos y el thread Profesor compartirán el objeto Nota:

import java.io.* ;

class escuela {

	public static void main ( String args[] ){

		try {

			// Generamos el objeto Nota, el profesor

			// y los dos alumnos …

			Nota laNota = new Nota ();

			Profesor p = new Profesor ( laNota );

			Alumno a = new Alumno ( "Javi", laNota);

			Alumno b = new Alumno ( "Jose", laNota );

			// Empezamos la ejecución

			a.start();

			b.start();

			p.start();

			a.join();

			b.join();

			p.join();

		}

		catch ( Exception e ){

			System.out.println ( e.toString() );

		}

	}

}

class Alumno extends Thread{

	Nota na ;  // nota del alumno

	String nom ;  // nombre


	Alumno ( String nombre , Nota n )	{

		na = n ;

		nom = nombre ;

	}


	public void run ()	{	

		System.out.println ( nom + " Esperado su nota" );

		na.esperar(); // el alumno espera la nota

		System.out.println ( nom + " recibio su nota");	}

}

class Profesor extends Thread{

	Nota na ;


	Profesor (  Nota n ){
		na = n ;

	}

	public void run ()	{	

		System.out.println ( " Voy a poner la nota ");

	na.dar ();  // el profesor pone la nota del alumno
	}


}


class Nota {

	synchronized void esperar ()	{

		try {

			wait();		

		}

		catch (InterruptedException e ){

			;

		}

	}		

	synchronized void dar (){

		notifyAll();

	}

}

El resultado que se obtiene es el siguiente:

Javi Esperado su nota Jose Esperado su nota Voy a poner la nota Javi recibio su nota Jose recibio su nota

Expliquemos la manera en que se desarrolla el programa. Una vez que se han generado los threads para los alumnos y el profesor, los alumnos ejecutan el método na.esperar() por lo que llaman al método wait() y quedan esperando a que el profesor ponga la nota y llame al método notify(). Esto lo realiza en el método na.dar().

Obsérvese que no se ha ejecutado notify() sino notifyAll(). Si se hubiera ejecutado el primero se habría desbloqueado a un thread, a un único alumno, y el otro quedaría esperando. Puesto que lo que hay que hacer es desbloquear más de un thread se utiliza el método notifyAll().

Los threads Alumnos, una vez que han sido desbloqueados por el profesor ordenadamente, reciben su nota, es decir, continúan su ejecución en la línea siguiente de código.

EL EJEMPLO CLÁSICO DEL CONSUMIDOR Y PRODUCTOR

El ejemplo clásico en el tema de la comunicación entre dos threads es el del consumidor y el productor. Un thread productor genera un mensaje y lo pone en una cola. Mientras otro thread, el consumidor, recoge ese mensaje y lo visualiza. Tenemos un productor que tiene una cola limitada para generar mensajes y para desarrollar un ejemplo claro, haremos que el consumidor sea más lento que el productor, por lo que éste a veces tendrá que parar hasta que el consumidor acabe con algunos mensajes de la cola y se lo comunique .

Por tanto, tendremos un thread productor que genera una serie de mensajes, parando si llena una cola. Cada vez que genera un mensaje se lo notifica al consumidor y continúa con su trabajo, generando más mensajes.

Por el otro lado, el consumidor se encarga de recibir los mensajes y mostrarlos, por lo que dormirá mientras no tenga nada que recibir. Ahora bien, ¿qué ocurre cuando el productor esta durmiendo debido a que el consumidor es muy lento?. Pasemos a ver el código de ejemplo:

import java.util.Vector;

import java.util.Date;

class productor extends Thread {

	static final int MAXELEM=1;

	private Vector mensajes = new Vector();


	public void run() {

		try {

		    while ( true ) {

		     crearMensaje();  //genera mensaje

		     sleep ( 1000 );  // a dormir

		   }

		}

		catch ( InterruptedException e ) { }
	}


	private synchronized void crearMensaje()  
 throws InterruptedException
	{

		// Mientras que la cola este llena, a esperar ...
		while ( mensajes.size() == MAXELEM )

		wait();


		// genero el mensaje, lo añado a la cola

		mensajes.addElement ( new Date().toString() );
		

		// Aviso al consumidor de que hay un nuevo mensaje

		notify();
	}


	// Atencion, este método es llamado SOLO por el 
    CONSUMIDOR

	public synchronized String recibirMensaje() 
 throws InterruptedException

	{


		// Mientras no haya nada que recoger ... 
     dormir ...		

		while ( mensajes.size() == 0 )

			wait();

			

		// Eliminamos el mensaje leido y lo devolvemos 

		String  cadMensaje = (String) 
  mensajes.firstElement();

		mensajes.removeElement( cadMensaje );


		// Aviso al productor que se ponga a trabajar
     si se durmió ya que puede añadir un elemento

		notify();


		return cadMensaje;

	}

}


class consumidor extends Thread {

	productor elProductor ;

	

	consumidor ( productor unProductor ) {

		elProductor = unProductor ;

	}


	public void run () {

		try {

		    while ( true ) {

   			String cadMensaje= elProductor.recibirMensaje();
 
   			System.out.println ( " Recibido del productor "
      + cadMensaje );

			sleep( 2000 );  // Consumidor a dormir ... 

		    }

		}		

		catch ( InterruptedException e ) { }
	}


	public static void main ( String args[] )	{

		productor miProductor = new productor();

		miProductor.start();

		new consumidor( miProductor ).start();

	}	

}

Ejecutamos el código y obtenemos lo siguiente:

Recibido del productor Sat Jun 27 18:34:51 GMT+02:00 1998

Recibido del productor Sat Jun 27 18:34:52 GMT+02:00 1998

Recibido del productor Sat Jun 27 18:34:53 GMT+02:00 1998

Recibido del productor Sat Jun 27 18:34:55 GMT+02:00 1998

Analicemos el código. El código del productor consta de una cola de mensajes y se dedica a crear mensajes y almacenarlos. Tras esto se pone a dormir. El método crearMensaje() se encarga de la creación de los mismos, comprobando si ya la cola de mensajes ya está llena, ya que si no tendría que bloquearse al no poder crear más mensajes. Este bloqueo estaría activo hasta que el consumidor avisara al productor de que no tiene nada que recoger. Además, el método almacena el mensaje y comunica el consumidor que ya tiene la posibilidad de recoger un mensaje nuevo.

El método recibirMensaje() que es invocado por el consumidor, nunca por el productor, se encarga de la tarea contraria. En caso de que no haya ningún mensaje, el consumidor espera, por lo que se bloquea llamando a wait(). Una vez que hay un mensaje y el productor lo ha desbloqueado, lo recoge y lo elimina de la cola del productor. A su vez, siempre que elimina un elemento notifica al productor que despierte por si está durmiendo y así éste se pone a generar nuevos mensajes. Finaliza visualizándolo y haciendo una pausa, ya que hemos hecho que el productor sea más veloz que el consumidor.

Vamos a añadir un segundo consumidor al programa, para observar qué ocurre. Cambiamos la clase consumidor de forma que contenga el siguiente código y modificamos el método crearMensaje del productor para que su notificación sea una llamada al método notifyAll():

private synchronized void crearMensaje()  throws InterruptedException

	{

		// Mientras que la cola este llena, a esperar ...

		while ( mensajes.size() == MAXELEM )

			wait();


		// genero el mensaje, lo añado a la cola

		mensajes.addElement ( new Date().toString() );

		

		// Aviso a los consumidores de que hay un 
     nuevo mensaje

		notifyAll();  

	}


class consumidor extends Thread {

	productor elProductor ;

	String nombre;

	

	consumidor ( String pNombre, productor 
 unProductor ) {

		elProductor = unProductor ;

		nombre = pNombre;

	}


	public void run () {

		try {

		    while ( true ) {

   			String cadMensaje= elProductor.recibirMensaje();

			System.out.println ( nombre + " recibe " + 
   cadMensaje );

			sleep( 2000 );  // Consumidor a dormir ... 

		    }

		}		

		catch ( InterruptedException e ) { }			

	}


	public static void main ( String args[] )	{

		productor miProductor = new productor();

		miProductor.start();

		new consumidor( "consumidor1", miProductor ).start();

		new consumidor( "consumidor2", miProductor ).start();

	}	

}

La ejecución muestra la siguiente salida por pantalla tal y como la exponemos en el código que describimos a continuación:

consumidor1 recibe Sat Jun 27 19:56:40 GMT+02:00 1998

consumidor2 recibe Sat Jun 27 19:56:41 GMT+02:00 1998

consumidor1 recibe Sat Jun 27 19:56:42 GMT+02:00 1998

consumidor2 recibe Sat Jun 27 19:56:43 GMT+02:00 1998

Como se observa se alternan ordenadamente los dos consumidores como resultado de la llamada sleep() en los distintos métodos. Es un buen ejercicio comprobar qué sucede si se eliminan las llamadas a sleep(), ya que según la implementación Java que estemos utilizando observaremos diferentes resultados.

Además, se puede comprobar que no es necesaria modificación alguna sobre el método crearMensaje() del productor, es decir, la sentencia notifyAll() puede ser también notify() ya que si un consumidor está esperando a que aparezca un mensaje en la cola, no es posible que el productor este esperando simultáneamente porque la cola esté llena.

Como se ha detallado, el uso de synchronized y el par notify/wait permite a los thread un tratamiento correcto de los datos compartidos, es decir, una sincronización de acceso y tratamiento. Como hemos visto, synchronized resulta más conveniente para controlar el acceso simultaneo a un mismo método de un objeto. El par notify/wait permite a los threads una sincronización más definida, además de unos mecanismos de bloqueo basados en la espera y notificación.

UN THREAD ESPECIAL, EL THREAD "DAEMON"

Un thread Daemon (demonio) es un thread de baja prioridad que consta de un bucle infinito y se suele utilizar para prestar servicios cuando se necesite, por ejemplo, refresco de memoria, mostrar imágenes, etc. Java utiliza un thread daemon, el recolector de basura para realizar todas las gestiones sobre la creación y destrucción de los objetos en memoria.

Un thread demonio se declara de la siguiente manera:

Thread.setDaemon();


y se puede comprobar si un thread es tipo "demonio" mediante el siguiente método:

isDaemon();

La diferencia entre los threads normales y los demonios es que Java no espera la muerte de los demonios para detener su ejecución. Los threads normales impiden al núcleo de Java parar el programa principal hasta que estos no mueren bien por la llamada a stop() o por la terminación normal del thread. En el caso de los demonios, Java los elimina cuando comprueba que únicamente quedan threads de del tipo demonios. Su ventaja consiste en que no es necesario controlar su finalización, de tal forma que se evitan los clásicos procedimientos de pararCopiar(), detenerSpool(), etc.

Otra característica consiste en que cualquier thread que se crea desde otro que es daemon también es un thread demonio.

El siguiente ejemplo demuestra la creación de threads demonios:

import java.io.*;


class demonio extends Thread {

    private int MAXELEM = 10 ;

    private Thread [] t = new Thread [MAXELEM];

    public demonio () {

        setDaemon(true);

       start();

    }

	

    public void run() {

        for ( int i =0 ; i < MAXELEM ; i ++)

            t[i] = new demonioHijo(i);

        for ( int i =0 ; i < MAXELEM ; i ++) {

            System.out.println ( " t[ "+i+ "] 
            is.Daemon() = " + t[i].isDaemon() );

        }

        while (true)  yield();

    }

}


class demonioHijo extends Thread {

    public demonioHijo ( int i ) {

       System.out.println ( " demonioHijo " + i 
       + " iniciado ");

       start();

    }

    public void run() {

        while (true)  yield();


    }

}


public class misdemonios {

    public static void main ( String [] args) throws 
    IOException

    {

        Thread d = new demonio();

        System.out.println ( "d.isDaemon() " + d.isDaemon() 

	   System.in.read();

);

}

CONCLUSIONES

Como se ha podido comprobar a lo largo de este artículo, el lenguaje Java nos permite unos sencillos y efectivos mecanismos de sincronización y comunicación entre threads, que van desde el uso de la palabra clave synchronized hasta el par wait/notify.

Gracias a estos métodos se puede controlar el desarrollo de aplicaciones que utilicen recursos compartidos entre múltiples threads. Además hemos aprendido a desarrollar los threads "demonios", tan comunes en lenguajes y sistemas operativos, con el fin de tener la posibilidad de ofrecer servicios a otros threads.



Banner 468x60
Explorer 4.0, Netscape 4.0. Resolución 800 x 600.
©Tower Communications 1.998.
Diseño: GRUPO ALBERTINA DE COMUNICACION.