Sincronizacion de Threads Java

8
Sincronización de Threads Las lecciones anteriores contenían ejemplos con threads asíncronos e independientes. Esto es, cada thread contenía todos los datos y métodos necesarios y no requerian recursos externos. Además, 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 programación 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 aplicación 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 común; el primero comparte un fichero y el segundo una pila de eventos. Como los threads comparten un recurso común, deben sincronizarse de alguna forma. Esta lección enseña la sincronización 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 número generado. Para hacer más interesante el problema de la sincronización, el prodcutor duerme durante un tiempo aleatorio entre 0 y 100 milisegundos antes de repetir el ciclo de generación de números. 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, estándo hambriento, consume todos los enteros de CubbyHole (exactamenten el mismo objeto en que el productor puso los enteros en primer lugar) tan rápidamente como estén disponibles. class Consumer extends Thread { private CubbyHole cubbyhole; private int number; public Consumer(CubbyHole c, int number) {

description

Sincronizacion hilos java productor consumidor

Transcript of Sincronizacion de Threads Java

Page 1: Sincronizacion de Threads Java

Sincronización de Threads

Las lecciones anteriores contenían ejemplos con threads asíncronos e independientes. Esto es, cada thread conteníatodos los datos y métodos necesarios y no requerian recursos externos. Además, los threads de esos ejemplos seejecutaban en su propio espacio sin concernir sobre el estado o actividad de otros threads que se ejecutaban deforma concurrente.

Sin embargo, existen muchas situaciones interesantes donde ejecutar threads concurrentes que compartan datos ydeban considerar el estado y actividad de otros threads. Este conjunto de situaciones de programación sonconocidos como escenarios 'productor/consumidor'; donde el productor genera un canal de datos que es consumidopor el consumidor.

Por ejemplo, puedes imaginar una aplicación Java donde un thread (el productor) escribe datos en un ficheromientras que un segundo thread (el consumidor) lee los datos del mismo fichero. O si tecleas caracteres en elteclado, el thread productor situa las pulsaciones en una pila de eventos y el thread consumidor lee los eventos de lamisma pila. Estos dos ejemplos utilizan threads concurrentes que comparten un recurso común; el primero comparteun fichero y el segundo una pila de eventos.

Como los threads comparten un recurso común, deben sincronizarse de alguna forma.

Esta lección enseña la sincronización 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 númerogenerado. Para hacer más interesante el problema de la sincronización, el prodcutor duerme durante un tiempoaleatorio entre 0 y 100 milisegundos antes de repetir el ciclo de generación de números.

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, estándo hambriento, consume todos los enteros de CubbyHole (exactamenten el mismo objeto enque el productor puso los enteros en primer lugar) tan rápidamente como estén disponibles.

class Consumer extends Thread {private CubbyHole cubbyhole;private int number;

public Consumer(CubbyHole c, int number) {

Page 2: Sincronizacion de Threads Java

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 través de un objeto CubbyHole común.Observaráa que ninguno de los dos hace ningún esfuerzo sea el que sea para asegurarse de que el consumidorobtiene cada valor producido una y sólo una vez. La sincronización entre estos dos threads realmente ocurre a unnivel inferior, dentro de los métodos get() y put() del objeto CubbyHole. Sin embargo, asumamos por un momentoque estos dos threads no están sincronizados y veamos los problemas potenciales que podría provocar estasituación.

Un problema sería cuando el Productor fuera más rápido que el Consumidor y generara dos números antes de que elConsumidor tuviera una posibilidad de consumir el primer número. Así el Consumidor se saltaría un número. Parte dela salida se podría parecer a esto.

. . .Consumidor #1 obtiene: 3Productor #1 pone: 4Productor #1 pone: 5Consumidor #1 obtiene: 5

. . .

Otro problema podría aparecer si el consumidor fuera más rápido que el Productor y consumiera el mismo valor doso más veces. En esta situación el Consumidor imprimirá el mismo valor dos veces y podría producir una salida comoesta.

. . .Productor #1 pone: 4Consumidor #1 obtiene: 4Consumidor #1 obtiene: 4Productor #1 pone: 5

. . .

De cualquier forma, el resultado es erróneo. Se quiere que el consumidor obtenga cada entero producido por elProductor y sólo una vez. Los problemas como los escritos anteriormente,se llaman condiciones de carrera. Sealcanzan cuando varios threads ejecutados asíncronamente intentan acceder a un mismo objeto al mismo tiempo yobtienen resultados erróneos.

Para prevenir estas condiciones en nuestro ejemplo Productor/Consumidor, el almacenamiento de un nuevo enteroen CubbyHole por el Productor debe estar sincronizado con la recuperación del entero por parte del Consumidor. ElConsumidor debe consumir cada entero exactamente una vez. El programa Productor/Consumidor utiliza dosmecanismos diferentes para sincronizar los threads Producer y Consumer; los monitores, y los métodos notify() ywait().

Monitores

Los objetos, como el CubbyHole que son compartidos entre dos threads y cuyo acceso debe ser sincronizado sonllamados condiciones variables. El lenguaje Java permite sincronizar threads alrededor de una condición variablemediante el uso de monitores. Los monitores previenen que dos threads accedan simultáneamente a la mismavariable.

Page 3: Sincronizacion de Threads Java

Los métodos notify() y wait()

En un nivel superior, el ejemplo Productor/Consumidor utiliza los métodos notify() y wait() del objeto para coordinarla activadad de los dos threads. El objeto CubyHole utiliza notify() y wait() para asegurarse de que cada valorsituado en él por el Productor es recuperado una vez y sólo una por el Consumidor.

El programa Principal

Aquí tienes una pequeña aplicación Java que crea un objeto CubbyHole, un Producer, un Consumer y arranca losdos 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: 0Consumidor #1 obtiene: 0Productor #1 pone: 1Consumidor #1 obtiene: 1Productor #1 pone: 2Consumidor #1 obtiene: 2Productor #1 pone: 3Consumidor #1 obtiene: 3Productor #1 pone: 4Consumidor #1 obtiene: 4Productor #1 pone: 5Consumidor #1 obtiene: 5Productor #1 pone: 6Consumidor #1 obtiene: 6Productor #1 pone: 7Consumidor #1 obtiene: 7Productor #1 pone: 8Consumidor #1 obtiene: 8Productor #1 pone: 9Consumidor #1 obtiene: 9

Monitores Java

El lenguaje Java y el sistema de ejecución soportan la sincronizaxión de threads mediante el uso de monitores. Engeneral, un monitor está asociado con un objeto especifico (una condición variable) y funciona como un bloqueo paraese dato. Cuando un thread mantiene el monitor para algún dato del objeto, los otros threads están bloqueados y nopueden ni inspeccionar ni modificar el dato.

Page 4: Sincronizacion de Threads Java

Los segmentos de código dentro de programa que acceden al mismo dato dentro de threads concurrentes separadosson conocidos como secciones críticas. En el lenguaje Java, se pueden marcar las secciones críticas del programacon la palabra clave synchronized.

Nota: Generalmente, la sección críticas en los programas Java son métodos. Se pueden marcar segmentospequeños de código como sincronizados.

Sin embargo, esto viola los paradigmas de la programación orientada a objetos y produce un código que es díficil deleer y de mantener. Para la mayoría de los propósitos de programación en Java, es mejor utilizar synchronized sóloa nivel de métodos.

En el lenguaje Java se asocia un único monitor con cada objeto que tiene un método sincronizado. La claseCubbyHole del ejemplo Producer/Consumer de la página anterior tiene dos métodos sincronizados: el método put(),que se utiliza para cambiar el valor de CubbyHole, y el método 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 código fuente del objeto CubbyHole. Las líneas en negrita proporcionan la sincronización de losthreads.

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 variablebooleana available, que indica si se puede recuperar el contenido de CubbyHole. Cuando available es verdaderaindica que el Productor ha puesto un nuevo valor en CubbyHole y que el Consumidor todavía no la ha consumido. ElConsumidor sólo puede consumir el valor de CubbyHole cuando available es verdadera.

Como CubbyHole tiene dos métodos sincronizados, java proporciona un único monitor para cada ejemplar deCubbyHole (incluyendo el compartido por el Productor y el Consumidor). Siempre que el control entra en un métodosincronizado, el thread que ha llamado el método adquiere el monitor del objeto cuyo método ha sido llamado. Otrosthreads no pueden llamar a un método sincronizado del mismo objeto hasta que el monitor sea liberado.

Nota:

Page 5: Sincronizacion de Threads Java

Los Monitores Java son Re-entrantes.

Es decir, el mismo thread puede llamar a un método sincronizado de un objeto para elque ya tiene el monitor, es decir, puede re-adquirir el monitor.

Así, siempre que el Productor llama al método put() de CubbyHole, adquiere el monitor del objeto CubbyHole, y asíevita que el consumidor pueda llamar al método get() de CubbyHole. (El método wait() libera temporalmente elmonitor como se verá más 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 método put() retorna, el Productor libera el monitor y por lo tanto desbloquea el objeto CubbyHole.

Siempre que el Consumidor llama al método get() de CubbyHole, adquiere el monitor de ese objeto y por lo tantoevita que el productor pueda llamar al método 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 adquisición y liberación del monitor la hace automáticamente el sistema de ejecución de Java. Esto asegura queno puedan ocurrir condiciones de competición en la implementación de los threads, asegurando la integridad de losdatos.

Prueba esto: Elimina las líneas que están en negrita en el listado de la clase CubbyHole mostrada arriba. Recompilael programa y ejecutalo de nuevo. ¿Qué sucede? Como no se ha realizado ningún esfuerzo explícito para sicronizarlos threads, el Consumidor consume con un abandono temerario y obtiene sólo una ristra de ceros, en lugar deobtener los enteros entre 0 y 9 exactamente una vez cada uno.

Los Monitores Java son Re-Entrantes

Page 6: Sincronizacion de Threads Java

El sistema de ejecución de Java permite que un thread re-adquiera el monitor que ya posee realmente porque losmonitores Java son re-entrantes. Los monitores re-entrantes son importantes porque eliminan la posibilidad de queun sólo 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 métodos sincronizados: a() y b(). El primer método sincronizado, llama al otro métodosincronizado.

Cuando el control entra en el método a(), el thread actual adquiere el monitor del objeto Reentrant. Ahora, a() llama ab() y como también está sincronizado el thread también intenta adquirir el monitor de nuevo. Como Java soporta losmonitores re-entrantes, esto si funciona. El thread actual puede adquirir de nuevo el monitor del objeto Reentrant y seejecutan los dos métoso 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 métodos causaría un puntomuerto.

Los Métodos Wait() y Notify()

Los métodos get() y put() del objeto CubbyHole hacen uso de los métodos notify() y wait() para coordinar laobtención y puesta de valores dentro de CubbyHole. Los métodos notify() y wait() son miembros de la clasejava.lang.Object.

Nota:

Los métodos notify() y wait() pueden ser invocados sólo desde dentro de un métodosincronizado o dentro de un bloque o una sentencia sincronizada.

Investiguemos el uso del método notify() en CubbyHole mirando el método get().

El método notify()

El método get() llama al método notify() como lo último que hace (junto retornar). El método notify() elige un threadque está esperando el monitor poseido por el thread actual y lo despierta. Normalmente, el thread que esperacapturará el monitor y procederá.

Page 7: Sincronizacion de Threads Java

El caso del ejemplo Productor/Consumidor, el thread Consumidor llama al método get(), por lo que el métodoConsumidor posee el monitor de CubbyHole durante la ejecución del método get(). Al final del método get(), lallamada al método 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 Productorreturn contents;

}

Si existen varios threads esperando por un monitor, el sistema de ejecución Java elige uno de esos threads, sinningún compromiso ni garantía sobre el thread que será eligido.

El método put() trabaja de un forma similar a get(), despertanto al thread consumidor que está esperando que elProductor libere el monitor.

La clase Object tiene otro método --notifyAll()-- que despierta todos lo threads que están esperando al mismomonitor. En esta Situación, los threads despertados compiten por el monitor. Uno de ellos obtiene el monitor y losotros vuelven a esperar.

El método wait()

El método wait() hace que el thread actual espere (posiblemente para siempre) hasta que otro thread se lo notifiqueo a que cambie un condición. Se utiliza el método wait() en conjunción con el método notify() para coordinar laactividad de varios threads que utilizan los mismos recursos.

El método get() contiene una sentencia while que hace un bucle hasta que available se convierte en true. Siavailable es false -- el Productor todavía no ha producido un nuevo número y el consumidor debe esperar -- elmétodo get() llama a wait().

El bucle while contiene la llamada a wait(). El método wait() espera indefinidamente hasta que llegue unanotificación del thread Productor. Cuando el método put() llama a notify(), el Consumidor despierta del estado deespera y continúa con el bucle. Presumiblemente, el Productor ya ha generado un nuevo número y el método get()cae al final del bucle y procede. Si el Productor no ha generado un nuevo número, get() vuelve al principio del bucle ycontinua espeando hasta que el Productor genere un nuevo número 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 método put() trabaja de un forma similar, esperando a que el thread Consumidor consuma el valor actual antes depermitir que el Productor genere uno nuevo.

Junto a la versión utilizada en el ejemplo de Productor/Consumidor, que espera indefinidamente una notificación, laclase Object contiene otras dos versiones del método wait().

Page 8: Sincronizacion de Threads Java

wait(long timeout)Espera una notificación o hasta que haya pasado el tiempo de espera --timeout se mide en milisegundos.

wait(long timeout, int nanos)Espera una notificación o hasta que hayan pasado timeout milisegundos mas nanos nanosegundos.

Los Monitores y los Métodos notify() y wait()

Habras observado un problema potencial en los métodos put() y get() de CubbyHole. Al principio del método get(), siel valor de CubbyHole no está disponible (esto es, el Productor no ha generado un nuevo número desde la última vezque el Consumidor lo consumió), luego el Consumidor espera a que el Productor ponga un nuevo número enCubbyHole. Aquí está la cuestión -- ¿cómo puede el Productor poner un nuevo valor dentro de CubbyHole si elConsumidor tiene el monitor? (El Consumidor posee el monitor de CubbyHole porque está dentro del método get()que está sincronizado).

Similarmente, al principio del método put(), si todavía no se ha consumido el valor, el Productor espera a que elConsumidor consuma el valor del CubbyHole. Y de nuevo llegamos a la cuestión -- ¿Cómo puede el consumidorobtener el valor de CubbyHole, si el Productor posee el monitor? (El productor posee el monitor de CubbyHoleporque está dentro dentro del método put() que está sincronizado).

Bien, los diseñadores del lenguaje Java también pensaron en esto. Cuando el thread entra en el método wait(), loque sucede al principio de los métodos put() y get, el monitor es liberado automáticamente, y cuando el thread saledel método wait(), se adquiere de nuevo el monitor. Esto le da una oportunidad al objeto que está esperando deadquirir el monitor y, dependiendo, de quién está esperando, consume el valor de CubbyHole o produce un nuevovalor para el CubbyHole.