Está en la página 1de 8

Sincronizacin de Threads

Las lecciones anteriores contenan ejemplos con threads asncronos e independientes. Esto es, cada thread contena todos los datos y mtodos necesarios y no requerian recursos externos. Adems, los threads de esos ejemplos se ejecutaban en su propio espacio sin concernir sobre el estado o actividad de otros threads que se ejecutaban de forma concurrente. Sin embargo, existen muchas situaciones interesantes donde ejecutar threads concurrentes que compartan datos y deban considerar el estado y actividad de otros threads. Este conjunto de situaciones de programacin son conocidos como escenarios 'productor/consumidor'; donde el productor genera un canal de datos que es consumido por el consumidor. Por ejemplo, puedes imaginar una aplicacin Java donde un thread (el productor) escribe datos en un fichero mientras que un segundo thread (el consumidor) lee los datos del mismo fichero. O si tecleas caracteres en el teclado, el thread productor situa las pulsaciones en una pila de eventos y el thread consumidor lee los eventos de la misma pila. Estos dos ejemplos utilizan threads concurrentes que comparten un recurso comn; el primero comparte un fichero y el segundo una pila de eventos. Como los threads comparten un recurso comn, deben sincronizarse de alguna forma. Esta leccin ensea la sincronizacin de threads Java mediante un sencillo ejemplo de productor/consumidor.

El Ejemplo Productor/Consumidor
El Productor genera un entero entre 0 y 9 (inclusive), lo almacena en un objeto "CubbyHole", e imprime el nmero generado. Para hacer ms interesante el problema de la sincronizacin, el prodcutor duerme durante un tiempo aleatorio entre 0 y 100 milisegundos antes de repetir el ciclo de generacin de nmeros.
class Producer extends Thread { private CubbyHole cubbyhole; private int number; public Producer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { for (int i = 0; i < 10; i++) { cubbyhole.put(i); System.out.println("Productor #" + this.number + " pone: " + i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } } } }

El Consumidor, estndo hambriento, consume todos los enteros de CubbyHole (exactamenten el mismo objeto en que el productor puso los enteros en primer lugar) tan rpidamente como estn disponibles.
class Consumer extends Thread { private CubbyHole cubbyhole; private int number; public Consumer(CubbyHole c, int number) {

cubbyhole = c; this.number = number; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = cubbyhole.get(); System.out.println("Consumidor #" + this.number + " obtiene: " + value); } } }

En este ejemplo el Productor y el Consumidor comparten datos a travs de un objeto CubbyHole comn. Observara que ninguno de los dos hace ningn esfuerzo sea el que sea para asegurarse de que el consumidor obtiene cada valor producido una y slo una vez. La sincronizacin entre estos dos threads realmente ocurre a un nivel inferior, dentro de los mtodos get() y put() del objeto CubbyHole. Sin embargo, asumamos por un momento que estos dos threads no estn sincronizados y veamos los problemas potenciales que podra provocar esta situacin. Un problema sera cuando el Productor fuera ms rpido que el Consumidor y generara dos nmeros antes de que el Consumidor tuviera una posibilidad de consumir el primer nmero. As el Consumidor se saltara un nmero. Parte de la salida se podra parecer a esto.
. . . Consumidor #1 obtiene: 3 Productor #1 pone: 4 Productor #1 pone: 5 Consumidor #1 obtiene: 5 . . .

Otro problema podra aparecer si el consumidor fuera ms rpido que el Productor y consumiera el mismo valor dos o ms veces. En esta situacin el Consumidor imprimir el mismo valor dos veces y podra producir una salida como esta.
. . . Productor #1 pone: 4 Consumidor #1 obtiene: 4 Consumidor #1 obtiene: 4 Productor #1 pone: 5 . . .

De cualquier forma, el resultado es errneo. Se quiere que el consumidor obtenga cada entero producido por el Productor y slo una vez. Los problemas como los escritos anteriormente,se llaman condiciones de carrera. Se alcanzan cuando varios threads ejecutados asncronamente intentan acceder a un mismo objeto al mismo tiempo y obtienen resultados errneos. Para prevenir estas condiciones en nuestro ejemplo Productor/Consumidor, el almacenamiento de un nuevo entero en CubbyHole por el Productor debe estar sincronizado con la recuperacin del entero por parte del Consumidor. El Consumidor debe consumir cada entero exactamente una vez. El programa Productor/Consumidor utiliza dos mecanismos diferentes para sincronizar los threads Producer y Consumer; los monitores, y los mtodos notify() y wait().

Monitores
Los objetos, como el CubbyHole que son compartidos entre dos threads y cuyo acceso debe ser sincronizado son llamados condiciones variables. El lenguaje Java permite sincronizar threads alrededor de una condicin variable mediante el uso de monitores. Los monitores previenen que dos threads accedan simultneamente a la misma variable.

Los mtodos notify() y wait()


En un nivel superior, el ejemplo Productor/Consumidor utiliza los mtodos notify() y wait() del objeto para coordinar la activadad de los dos threads. El objeto CubyHole utiliza notify() y wait() para asegurarse de que cada valor situado en l por el Productor es recuperado una vez y slo una por el Consumidor.

El programa Principal
Aqu tienes una pequea aplicacin Java que crea un objeto CubbyHole, un Producer, un Consumer y arranca los dos threads.
class ProducerConsumerTest { public static void main(String[] args) { CubbyHole c = new CubbyHole(); Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1); p1.start(); c1.start(); } }

La Salida
Aqu tienes la salida del programa ProducerConsumerTest.
Producer #1 pone: 0 Consumidor #1 obtiene: Productor #1 pone: 1 Consumidor #1 obtiene: Productor #1 pone: 2 Consumidor #1 obtiene: Productor #1 pone: 3 Consumidor #1 obtiene: Productor #1 pone: 4 Consumidor #1 obtiene: Productor #1 pone: 5 Consumidor #1 obtiene: Productor #1 pone: 6 Consumidor #1 obtiene: Productor #1 pone: 7 Consumidor #1 obtiene: Productor #1 pone: 8 Consumidor #1 obtiene: Productor #1 pone: 9 Consumidor #1 obtiene:

0 1 2 3 4 5 6 7 8 9

Monitores Java
El lenguaje Java y el sistema de ejecucin soportan la sincronizaxin de threads mediante el uso de monitores. En general, un monitor est asociado con un objeto especifico (una condicin variable) y funciona como un bloqueo para ese dato. Cuando un thread mantiene el monitor para algn dato del objeto, los otros threads estn bloqueados y no pueden ni inspeccionar ni modificar el dato.

Los segmentos de cdigo dentro de programa que acceden al mismo dato dentro de threads concurrentes separados son conocidos como secciones crticas. En el lenguaje Java, se pueden marcar las secciones crticas del programa con la palabra clave synchronized. Nota: Generalmente, la seccin crticas en los programas Java son mtodos. Se pueden marcar segmentos pequeos de cdigo como sincronizados. Sin embargo, esto viola los paradigmas de la programacin orientada a objetos y produce un cdigo que es dficil de leer y de mantener. Para la mayora de los propsitos de programacin en Java, es mejor utilizar synchronized slo a nivel de mtodos. En el lenguaje Java se asocia un nico monitor con cada objeto que tiene un mtodo sincronizado. La clase CubbyHole del ejemplo Producer/Consumer de la pgina anterior tiene dos mtodos sincronizados: el mtodo put(), que se utiliza para cambiar el valor de CubbyHole, y el mtodo get(), que se utiliza para el recuperar el valor actual. As el sistema asocia un nico monitor con cada ejemplar de CubbyHole. Aqu tienes el cdigo fuente del objeto CubbyHole. Las lneas en negrita proporcionan la sincronizacin de los threads.
class CubbyHole { private int contents; private boolean available = false; public synchronized int get() { while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; notify(); return contents; } public synchronized void put(int value) { while (available == true) { try { wait(); } catch (InterruptedException e) { } } contents = value; available = true; notify(); } }

La clase CubbyHole tiene dos variables privadas: contents, que es el contenido actual de CubbyHole, y la variable booleana available, que indica si se puede recuperar el contenido de CubbyHole. Cuando available es verdadera indica que el Productor ha puesto un nuevo valor en CubbyHole y que el Consumidor todava no la ha consumido. El Consumidor slo puede consumir el valor de CubbyHole cuando available es verdadera. Como CubbyHole tiene dos mtodos sincronizados, java proporciona un nico monitor para cada ejemplar de CubbyHole (incluyendo el compartido por el Productor y el Consumidor). Siempre que el control entra en un mtodo sincronizado, el thread que ha llamado el mtodo adquiere el monitor del objeto cuyo mtodo ha sido llamado. Otros threads no pueden llamar a un mtodo sincronizado del mismo objeto hasta que el monitor sea liberado.

Nota:

Los Monitores Java son Re-entrantes. Es decir, el mismo thread puede llamar a un mtodo sincronizado de un objeto para el que ya tiene el monitor, es decir, puede re-adquirir el monitor.
As, siempre que el Productor llama al mtodo put() de CubbyHole, adquiere el monitor del objeto CubbyHole, y as evita que el consumidor pueda llamar al mtodo get() de CubbyHole. (El mtodo wait() libera temporalmente el monitor como se ver ms adelante).
public synchronized void put(int value) { // El productor adquiere el monitor while (available == true) { try { wait(); } catch (InterruptedException e) { } } contents = value; available = true; notify(); // El productor libera el monitor }

Cuando el mtodo put() retorna, el Productor libera el monitor y por lo tanto desbloquea el objeto CubbyHole. Siempre que el Consumidor llama al mtodo get() de CubbyHole, adquiere el monitor de ese objeto y por lo tanto evita que el productor pueda llamar al mtodo put().
public synchronized int get() { // El consumidor adquier el monitor while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; notify(); return contents; // el Consumidor libera el monitor }

La adquisicin y liberacin del monitor la hace automticamente el sistema de ejecucin de Java. Esto asegura que no puedan ocurrir condiciones de competicin en la implementacin de los threads, asegurando la integridad de los datos. Prueba esto: Elimina las lneas que estn en negrita en el listado de la clase CubbyHole mostrada arriba. Recompila el programa y ejecutalo de nuevo. Qu sucede? Como no se ha realizado ningn esfuerzo explcito para sicronizar los threads, el Consumidor consume con un abandono temerario y obtiene slo una ristra de ceros, en lugar de obtener los enteros entre 0 y 9 exactamente una vez cada uno.

Los Monitores Java son Re-Entrantes

El sistema de ejecucin de Java permite que un thread re-adquiera el monitor que ya posee realmente porque los monitores Java son re-entrantes. Los monitores re-entrantes son importantes porque eliminan la posibilidad de que un slo thread ponga en punto muerto un monitor que ya posee. Consideremos esta clase.
class Reentrant { public synchronized void a() { b(); System.out.println("Estoy aqu, en a()"); } public synchronized void b() { System.out.println("Estoy aqu, en b()"); } }

Esta clase contiene dos mtodos sincronizados: a() y b(). El primer mtodo sincronizado, llama al otro mtodo sincronizado. Cuando el control entra en el mtodo a(), el thread actual adquiere el monitor del objeto Reentrant. Ahora, a() llama a b() y como tambin est sincronizado el thread tambin intenta adquirir el monitor de nuevo. Como Java soporta los monitores re-entrantes, esto si funciona. El thread actual puede adquirir de nuevo el monitor del objeto Reentrant y se ejecutan los dos mtoso a() y b(), como evidencia la salida del programa.
Estoy aqu, en b() Estoy aqu, en a()

En sistemas que no soportan monitores re-entrantes, esta secuencia de llamadas a mtodos causara un punto muerto.

Los Mtodos Wait() y Notify()


Los mtodos get() y put() del objeto CubbyHole hacen uso de los mtodos notify() y wait() para coordinar la obtencin y puesta de valores dentro de CubbyHole. Los mtodos notify() y wait() son miembros de la clase java.lang.Object.

Nota: Los mtodos notify() y wait() pueden ser invocados slo desde dentro de un mtodo sincronizado o dentro de un bloque o una sentencia sincronizada.
Investiguemos el uso del mtodo notify() en CubbyHole mirando el mtodo get().

El mtodo notify()
El mtodo get() llama al mtodo notify() como lo ltimo que hace (junto retornar). El mtodo notify() elige un thread que est esperando el monitor poseido por el thread actual y lo despierta. Normalmente, el thread que espera capturar el monitor y proceder.

El caso del ejemplo Productor/Consumidor, el thread Consumidor llama al mtodo get(), por lo que el mtodo Consumidor posee el monitor de CubbyHole durante la ejecucin del mtodo get(). Al final del mtodo get(), la llamada al mtodo notify() despierta al thread Productor que obtiene el monitor de CubbyHole y procede.
public synchronized int get() { while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; notify(); // lo notifica al Productor return contents; }

Si existen varios threads esperando por un monitor, el sistema de ejecucin Java elige uno de esos threads, sin ningn compromiso ni garanta sobre el thread que ser eligido. El mtodo put() trabaja de un forma similar a get(), despertanto al thread consumidor que est esperando que el Productor libere el monitor. La clase Object tiene otro mtodo --notifyAll()-- que despierta todos lo threads que estn esperando al mismo monitor. En esta Situacin, los threads despertados compiten por el monitor. Uno de ellos obtiene el monitor y los otros vuelven a esperar.

El mtodo wait()
El mtodo wait() hace que el thread actual espere (posiblemente para siempre) hasta que otro thread se lo notifique o a que cambie un condicin. Se utiliza el mtodo wait() en conjuncin con el mtodo notify() para coordinar la actividad de varios threads que utilizan los mismos recursos. El mtodo get() contiene una sentencia while que hace un bucle hasta que available se convierte en true. Si available es false -- el Productor todava no ha producido un nuevo nmero y el consumidor debe esperar -- el mtodo get() llama a wait(). El bucle while contiene la llamada a wait(). El mtodo wait() espera indefinidamente hasta que llegue una notificacin del thread Productor. Cuando el mtodo put() llama a notify(), el Consumidor despierta del estado de espera y contina con el bucle. Presumiblemente, el Productor ya ha generado un nuevo nmero y el mtodo get() cae al final del bucle y procede. Si el Productor no ha generado un nuevo nmero, get() vuelve al principio del bucle y continua espeando hasta que el Productor genere un nuevo nmero y llame a notify().
public synchronized int get() { while (available == false) { try { wait(); // espera una llamada a notify() desde el Productor } catch (InterruptedException e) { } } available = false; notify(); return contents; }

El mtodo put() trabaja de un forma similar, esperando a que el thread Consumidor consuma el valor actual antes de permitir que el Productor genere uno nuevo. Junto a la versin utilizada en el ejemplo de Productor/Consumidor, que espera indefinidamente una notificacin, la clase Object contiene otras dos versiones del mtodo wait().

wait(long timeout) Espera una notificacin o hasta que haya pasado el tiempo de espera --timeout se mide en milisegundos. wait(long timeout, int nanos) Espera una notificacin o hasta que hayan pasado timeout milisegundos mas nanos nanosegundos.

Los Monitores y los Mtodos notify() y wait()


Habras observado un problema potencial en los mtodos put() y get() de CubbyHole. Al principio del mtodo get(), si el valor de CubbyHole no est disponible (esto es, el Productor no ha generado un nuevo nmero desde la ltima vez que el Consumidor lo consumi), luego el Consumidor espera a que el Productor ponga un nuevo nmero en CubbyHole. Aqu est la cuestin -- cmo puede el Productor poner un nuevo valor dentro de CubbyHole si el Consumidor tiene el monitor? (El Consumidor posee el monitor de CubbyHole porque est dentro del mtodo get() que est sincronizado). Similarmente, al principio del mtodo put(), si todava no se ha consumido el valor, el Productor espera a que el Consumidor consuma el valor del CubbyHole. Y de nuevo llegamos a la cuestin -- Cmo puede el consumidor obtener el valor de CubbyHole, si el Productor posee el monitor? (El productor posee el monitor de CubbyHole porque est dentro dentro del mtodo put() que est sincronizado). Bien, los diseadores del lenguaje Java tambin pensaron en esto. Cuando el thread entra en el mtodo wait(), lo que sucede al principio de los mtodos put() y get, el monitor es liberado automticamente, y cuando el thread sale del mtodo wait(), se adquiere de nuevo el monitor. Esto le da una oportunidad al objeto que est esperando de adquirir el monitor y, dependiendo, de quin est esperando, consume el valor de CubbyHole o produce un nuevo valor para el CubbyHole.