You are on page 1of 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.

You might also like