Subprocesos (threads) |
La palabra reservada synchronized
El modelo Productor/Consumidor
La palabra reservada synchronized se usa para indicar que ciertas partes del código, (habitualmente, una función miembro) están sincronizadas, es decir, que solamente un subproceso puede acceder a dicho método a la vez.
Cada método sincronizado posee una especie de llave que puede cerrar o abrir la puerta de acceso. Cuando un subproceso intenta acceder al método sincronizado mirará a ver si la llave está echada, en cuyo caso no podrá accederlo. Si método no tiene puesta la llave entonces el subproceso puede acceder a dicho código sincronizado.
Las siguientes porciones de código son ejemplos de uso del modificador synchronized
synchronized public void funcion1(){ //... }
public void funcion2(){ Rectangle rect; synchronized(rect){ rect.width+=2; } rect.height-=3; }
Un ejemplo que veremos más adelante es la sincronización de una porción de código usando el objeto this, en el interior de una función miembro denominada mover
public void mover(){ synchronized (this) { indice++; if (indice>= numeros.length) { indice=0; } } //... }
En la primera porción de código, hemos asegurado un método de modo que un sólo subproceso a la vez puede acceder a la funcion1. En la segunda y tercera porción de código, tenemos un bloque de código asegurado. La anchura width del rectángulo rect no puede ser modificada por varios subprocesos a la vez. La altura height del rectángulo no está dentro del bloque sincronizado y puede ser modificada por varios subprocesos a la vez. El objeto rect se usa en este caso como llave de dicho bloque de código, en el tercer ejemplo este papel lo representa this.
Se debe evitar la sincronización de bloques de código y sustituirlas siempre que sea posible por la sincronización de métodos, lo que está más de acuerdo con el espíritu de la programación orientada a objetos. Se debe tener en cuenta que la sincronización disminuye el rendimiento de una aplicación, por tanto, debe emplearse solamente donde sea estrictamente necesario.
thread2:
Productor.java, Consumidor.java, Buffer.java, ThreadApp2.java
En la página previa, los subprocesos eran independientes, cada subproceso contiene los recursos que le son necesarios para su ejecución, corren a su propio paso sin interesarse por el estado o las tareas de los otros subprocesos que se ejecutan a la vez. Sin embargo, hay muchas situaciones interesantes en las que los subprocesos comparten datos y han de considerar el estado y las actividades de los otros subprocesos. Una de estas situaciones se denomina modelo Productor/Consumidor: el productor genera un flujo de datos que son recogidos por el consumidor. Cuando dos subprocesos comparten un recurso común han de estar sincronizados de algún modo.
Un problema se origina cuando el productor va más rápido que el consumidor y genera un segundo dato antes de que el consumidor tenga la oportunidad de recoger el primero. El consumidor se saltará un dato. Del mismo modo, si el consumidor es más rápido que el productor, puede que no tenga datos que recoger, o que recoja varias veces el mismo dato.
Cuando estamos diseñando subprocesos que compiten por recursos limitados hemos de tener cuidado en no caer en dos situaciones extremas, denominadas starvation (morir de hambre, el subproceso no progresa por falta de recursos) y deadlock (por ejemplo, cuando dos personas están en conflicto y uno está esperando a que el otro tome la iniciativa para resolverlo y viceversa)
La clase que describe el productor denominada Productor deriva de la clase Thread y redefine la función run. Tiene como miembro dato un objeto buffer de la clase Buffer que describiremos más adelante.
La función miembro run ejecuta un bucle for, cuando se completa el bucle se alcanza el final de run y el subproceso entra en el estado Death (muerto), y detiene su ejecución.
En el bucle for, se genera una letra al azar y se pone en el objeto buffer, llamando a la función poner de la clase Buffer. A continuación, se imprime y se hace un pausa por un número determinado de milisegundos llamando a la función sleep y pasándole el tiempo de pausa, durante este tiempo el subprocerso esta en el estado Not Runnable.
El bucle for no se suele utilizar sino un bucle while de la forma que se explicó en la página anterior
public class Productor extends Thread { private Buffer buffer; private final String letras="abcdefghijklmnopqrstuvxyz"; public Productor(Buffer buffer) { this.buffer=buffer; } public void run() { for(int i=0; i<10; i++){ char c=letras.charAt((int)(Math.random()*letras.length())); buffer.poner(c); System.out.println(i+" Productor: " +c); try { sleep(400); } catch (InterruptedException e) { } } } } |
La clase que describe al consumidor denominado Consumidor, deriva también de la clase Thread y redefine el método run. La definición de run es similar a la de la clase Productor, salvo que en vez de poner un carácter en el buffer, recoge el carácter guardado en el buffer intermedio llamando a la función recoger.
El tiempo de pausa (argumento de la función sleep) puede ser distinto en la clase Productor que en la clase Consumidor. Por ejemplo, para hacer que el productor sea más rápido que el consumidor, se pone un tiempo menor en la primera que en la segunda.
public class Consumidor extends Thread { private Buffer buffer; public Consumidor(Buffer buffer) { this.buffer=buffer; } public void run(){ char valor; for(int i=0; i<10; i++){ valor=buffer.recoger(); System.out.println(i+ " Consumidor: "+valor); try{ sleep(100); }catch (InterruptedException e) { } } } } |
El objeto compartido entre el productor y el consumidor está descrito por la clase denominada Buffer. Vamos a llegar a la definición de dicha clase en dos pasos sucesivos.
La clase Buffer tiene dos miembros dato: el primero contenido guarda un carácter (es el buffer), el segundo, disponible indica si el buffer está lleno o está vacío, según que ésta variable valga true o false.
La definición de las funciones poner y recoger es muy simple:
public class Buffer { private char contenido; private boolean disponible=false; public Buffer() { } public char recoger(){ if(disponible){ disponible=false; return contenido; } return ('\t'); } public void poner(char c){ contenido=c; disponible=true; } }
Definimos una clase que describe una aplicación. En la función main, creemos tres objetos un objeto b de la clase Buffer, un objeto p de la clase Productor y otro objeto c de la clase Consumidor. Al constructor de las clases Productor y Consumidor le pasamos el objeto b compartido de la clase Buffer.
Ponemos en marcha los subprocesos descritos por las clases Productor y Consumidor, mediante la llamada a la función start de modo que el estado de los subprocesos pasa de New Thread a Runnable. Los subprocesos una vez puestos en marcha mueren de muerte natural, pasando al estado Death después de ejecutar un bucle for de 10 iteracciones en la función run.
public class ThreadApp2 { public static void main(String[] args) { Buffer b=new Buffer(); Productor p=new Productor(b); Consumidor c=new Consumidor(b); p.start(); c.start(); } } |
Cuando corremos la aplicación observamos la siguiente salida
![]() |
![]() |
![]() |
La imagen de la izquierda corresponde a la situación en la que el consumidor va más rápido (100 como argumento de sleep) que el productor (400 como argumento de sleep). En la primera iteracción (líneas marcadas con 0), el consumidor va al buffer y no encuentra ninguna letra (inicialmente esta vacío), el productor pone en el buffer la letra f. En la segunda iteracción (líneas marcadas con 1) el consumidor consume la letra f, y el buffer se queda vacío, el consumidor vuelve a acceder de nuevo al buffer y no encuentra nada, etc.
La imagen del centro corresponde a la situación en la que el productor va más rápido (100 como argumento de sleep) que el consumidor (400 como argumento de sleep). En la primera iteracción (líneas marcadas con 0) el consumidor accede al buffer y lo encuentra vacío, el productor pone en el buffer el leta u. En sucesivas iteracciones el productor pone en el buffer las letras b, a continuación la sustituye por r y luego, por d. En la segunda iteracción (línea marcada por 1) del consumidor consume esta última letra d, etc.
En la tercera imagen, el productor y el consumidor tienen las misma rapidez (100 como argumento de sus funciones sleep). El productor y el consumidor están más coordinados, ya que la letra que pone el productor en el buffer es consumida por el consumidor. Esta situación es ideal y limitada a unas pocas iteracciones, ya que en general, los dos subprocesos ejecutan tareas distintas durante miles de iteracciones, por lo que no tienen la misma velocidad aún caundo el argumento de la función sleep sea el mismo.
Para resolver este problema los subprocesos han de estar sincronizados de dos modos. En primer lugar, se ha de poner el modificador synchronized delante de los métodos poner y recoger. Los dos subprocesos no pueden de este modo acceder simultáneamente al objeto buffer compartido.
Como ya hemos mencionado en el primer apartado, una sección crítica es un bloque de código o un método que es identificado por la palabra reservada synchronized. De este modo, el consumidor no puede acceder al objeto buffer cuando el productor lo está cambiando (llama a poner). El productor no podrá cambiarlo cuando el consumidor está obteniendo (llama a recoger) el valor que guarda dicho objeto.
En segundo lugar, se ha de mantener una cordinación entre el productor y el consumidor, de modo que cuando el productor ponga una letra en el buffer avise al consumidor de que el buffer está disponible para recoger dicha letra y viceversa, es decir, cuando el consumidor recoja la letra avise al productor de que el buffer está vacío. A su vez, el consumidor esperará hasta que el buffer esté lleno con una letra y el productor esperará hasta que el buffer esté nuevamente vacío para poner otra letra.
La clase Thread nos proporcionan los métodos wait, notify y notifyAll, para hacer que los subprocesos esperen hasta que se cumpla una determinada condición, y cuando se cumpla se notifica a otros subprocesos que la condición ha cambiado.
El código de las funciones miembro recoger y poner será ahora el siguiente:
Espera (wait) mientras (while) disponible sea false (buffer vacío). En caso contrario (disponible sea true o el buffer esté lleno), devuelve la letra contenida en el buffer (return contenido), poner de nuevo disponible en false (se ha vaciado el buffer al recoger la letra), y avisa (notify o notifyAll) a los otros subprocesos de este hecho.
Espera (wait) mientras (while) disponible sea true (buffer lleno). En caso contrario (disponible sea false o el buffer esté vacío), guarda en el buffer (miembro dato contenido) la letra que se le pasa en el parámetro de la función poner. Cambia el valor de disponible a true, y avisa (notify o notifyAll) a los otros subprocesos de este hecho.
El método wait de la clase Thread hace que el subproceso espere en un estado Not Runnable hasta que sea avisado (notify) de que continúe. El método notify informa al subproceso en espera que continúe su ejecución. notifyAll es similar a notify excepto que se aplica a todos los subprocesos en estado de espera. Estos métodos solamente se pueden llamar desde funciones sincronizadas (con modificador synchronized). Las llamadas a la función wait como sleep deben de estar dentro de un bloque try...catch.
El código de la clase Buffer será ahora el siguiente
public class Buffer { private char contenido; private boolean disponible=false; public Buffer() { } public synchronized char recoger(){ while(!disponible){ try{ wait(); }catch(InterruptedException ex){} } disponible=false; notify(); return contenido; } public synchronized void poner(char valor){ while(disponible){ try{ wait(); }catch(InterruptedException ex){} } contenido=valor; disponible=true; notify(); } } |
La salida del programa será ahora la adecuada, tal como se muestra en la figura inferior. El productor y el consumidor están ya coordinados.
El retraso en la carga de las imágenes y otros recursos desde un servidor remoto es una situación que experimentamos continuamente. El lenguaje Java dispone de una librería de clases que trata con eficacia este problema.
El modelo productor-consumidor tiene en cuenta la progresiva carga de las imágenes y supone que el productor genera los pixels de la imagen y el consumidor la muestra. Java utiliza también el concepto de filtro que permite cambiar el aspecto de la imagen a medida que pasa desde el productor al consumidor.
El modelo productor/consumidor tiene muchas ventajas entre las que cabe destacar la modularidad y la interacción asíncrona entre productor y consumidor. Esto último significa, que una vez que se ha conectado el productor con el consumidor, el productor notifica al consumidor solamente cuando hay información adicional disponible, mientras tanto, el applet o la aplicación pueden seguir haciendo otro trabajo.
Los productores representan la fuente de los datos situados al otro lado de la red. Los consumidores representan los applets o aplicaciones situados a este lado de la red. Su trabajo conjunto permite enviar los recursos desde un lado hacia el otro.
El interface ImageProducer tiene los siguientes métodos:
La clase que describe un objeto productor ha de implementar el interface ImageProducer. Como vemos los métodos requieren que se les pase un objeto cuya clase implemente el interface ImageConsumer. Ya que un productor no tiene sentido sin su consumidor asociado. Un productor puede tener múltiples consumidores que se añaden mediante addConsumer y se eleminan mediante removeConsumer.
La clase que describe el objeto consumidor (normalmente es uno) ha de implementar el interface ImageConsumer, que es más complejo.
Veamos el proceso de carga de una imagen proveniente de la Red.
Nombre | Significado |
RANDOMPIXELORDER=1 | No hace mención de la forma en la que se envían los pixels |
TOPDOWNLEFTRIGHT=2 | Para dibujar la imagen de arriba hacia abajo, desde la izquierda a la derecha |
COMPLETESCANLINES=4 | Se envían filas completas de pixels |
SINGLEPASS=8 | El envío de los pixels de la imagen se hace en una sola llamada a setPixels |
SINGLEFRAME=16 | Una sola imagen |
Nota: para un estudio de los filtros consultar uno de los libros
David M. Geary. Graphic Java 2. Volume I. AWT. Java Series. Sun Microsystems Press (1998)
David M. Geary. Graphic JAVA 1.1. Mastering the AWT, second edition. Sun Microsystems (1997).