Java-Programacion Multithread - Escuela de Ingeniería Industrial
Transcripción
Java-Programacion Multithread - Escuela de Ingeniería Industrial
¿Qué es un thread? Hasta el momento hemos desarrollado programas secuenciales con un único thread: en cualquier instante durante la ejecución de un programa hay un único punto de ejecución. Java: Programación Multithread Franco Guidi Polanco Un thread Escuela de Ingeniería Industrial Pontificia Universidad Católica de Valparaíso, Chile [email protected] Un programa Franco Guidi Polanco 09-03-2007 ¿Qué es un thread? (cont.) 2 Ejemplo Un thread es un flujo secuencial de control dentro de un programa Un programa puede tener más de un thread ejecutándose al mismo tiempo Dos threads Un programa Una aplicación que posee un thread que saluda y otro que se despide: Un programa for(int i = 1; i<100;i++) for(int i = 1; i<100;i++) System.out.printl(“Hola ” + i ); System.out.printl(“Chao ” + i ); Dos threads Franco Guidi Polanco 09-03-2007 3 Franco Guidi Polanco 09-03-2007 4 Ejemplo (cont.) Creación de múltiples threads en Java Los threads en Java se implementan por medio de la clase java.lang.Thread. Output posible: Hola Hola Chao Hola Chao Chao Hola … Franco Guidi Polanco 1 2 1 3 2 3 4 Existen dos formas de crear threads en Java: Extendiendo la clase java.lang.Thread Implementando la interfaz java.lang.Runnable 09-03-2007 5 Creación de threads extendiendo la clase java.lang.Thread 09-03-2007 09-03-2007 6 Creación de threads extendiendo la clase java.lang.Thread: ejemplo 1 El thread debe ser una clase que extiende la clase Thread Se debe sobreescribir el método run() con el código que deberá ser ejecutado por el thread. El thread debe ser lanzado invocando el método start() del objeto (heredado de la clase Thread) Franco Guidi Polanco Franco Guidi Polanco 7 Dos ejemplos de clases que extienden Thread: public class Hola extends Thread { public void run() { for (int i=1;i<100;i++) System.out.println( "Hola" + i ); } } public class Chao extends Thread { public void run() { for (int i=1;i<100;i++) System.out.println( "Chao" + i ); } } Franco Guidi Polanco 09-03-2007 8 Creación de threads extendiendo la clase java.lang.Thread: ejemplo 1 (cont.) ¿Qué ocurre en este ejemplo? La aplicación que lanza los threads: ... public static void main(String arg[]) { public class EjemploThreadSimple { public static void main(String arg[]) { Hola h = new Hola(); Chao c = new Chao(); h.start(); c.start(); System.out.println( “Fin programa” ); } } Hola h = new Hola(); Chao c = new Chao(); 09-03-2007 9 c.start(); System.out.println( “Fin programa” ); Thread Chao c.start(); Thread Hola ... } ... Franco Guidi Polanco Estos dos programas funcionan de igual modo 09-03-2007 10 11 public class EjemploThreadSimple { public static void main(String arg[]) { Hola h = new Hola(); Chao c = new Chao(); h.start(); c.start(); System.out.println( “Fin programa” ); } } public class EjemploThreadSimple { public static void main(String arg[]) { new Hola().start(); new Chao().start(); System.out.println( “Fin programa” ); } } NOTA: Ejecución en JVM sobre Windows XP. El resultado puede ser distinto en otras plataformas (se estudiará más adelante) 09-03-2007 h.start(); Si las referencias a los threads no son necesarias... Ejecución del ejemplo 1 Franco Guidi Polanco ... h.start(); Notar que para lanzar un nuevo thread se debe invocar el método start(). La simple invocación del método run()produce sólo su ejecución en el thread actual (como cualquier otro método). Franco Guidi Polanco Thread main Franco Guidi Polanco 09-03-2007 12 Creación de threads implementando la interfaz java.lang.Runnable La clase que contiene el proceso que será lanzado dentro de un nuevo thread debe implementar la interfaz java.lang.Runnable Particularmente se debe implementar en la clase el método run() declarado en Runnable, con el código que deberá ser ejecutado por el thread Para lanzar el thread se debe crear una instancia de java.lang.Thread, pasando al constructor de éste una referencia al objeto que implementa Runnable, y luego se debe invocar el método start() del objeto Thread Franco Guidi Polanco 09-03-2007 13 Creación de threads implementando la interfaz java.lang.Runnable: ejemplo 2 Dos ejemplos de clases que implementan Runnable: public class Hola implements Runnable { public void run() { for (int i=1;i<100;i++) System.out.println( "Hola" + i ); } } public class Chao implements Runnable { public void run() { for (int i=1;i<100;i++) System.out.println( "Chao" + i ); } } Franco Guidi Polanco 09-03-2007 15 Creación de threads implementando la interfaz java.lang.Runnable (cont.) Clase que implementa el código del thread: public class MiThread implements Runnable { ... public void run() { // código del thread } ... } Instanciación y lanzamiento del thread: ... Thread t = new Thread( new MiThread() ); t.start(); ... Franco Guidi Polanco 09-03-2007 14 Creación de threads implementando la interfaz java.lang.Runnable: ejemplo 2 (cont.) La aplicación que lanza los threads: public class EjemploThreadSimple { public static void main(String arg[]) { Thread h = new Thread( new Hola() ); Thread c = new Thread( new Chao() ); h.start(); c.start(); System.out.println( “Fin programa” ); } } Notar que para lanzar un nuevo thread se debe crear una instancia de Thread pasando al constructor una referencia a un objeto Runnable, e invocar el método start() sobre Thread. La simple invocación del método run() produce sólo su ejecución en el thread actual (como cualquier otro método). Franco Guidi Polanco 09-03-2007 16 (Nuevamente) si las referencias a los threads no son necesarias... Estos dos programas funcionan de igual modo Ejercicio public class EjemploThreadSimple { public static void main(String arg[]) { Thread h = new Thread( new Hola() ); Thread c = new Thread( new Chao() ); h.start(); c.start(); System.out.println( “Fin programa” ); } } public class EjemploThreadSimple { public static void main(String arg[]) { new Thread( new Hola() ).start(); new Thread( new Chao() ).start(); System.out.println( “Fin programa” ); } } Franco Guidi Polanco 09-03-2007 17 Cree un programa similar al ejemplo anterior que, en vez de usar clases distintas para manejar los threads correspondientes a los diferentes mensajes (“Hola, “Chao”), utilice dos thread de una misma clase (cuyo mensaje sea configurable). Haga una implementación del thread extendiendo la clase Thread y otra implementando Runnable. Haga la aplicación que lance los threads. Franco Guidi Polanco Solución 09-03-2007 Solución (cont.) Versión que extiende Thread: Versión que implementa Runnable: public class Habla extends Thread { String mensaje; public Habla(String msg){ mensaje = msg; } public void run() { for (int i=1;i<100;i++) System.out.println( mensaje + i ); } } public class Habla implements Runnable { String mensaje; public Habla(String msg){ mensaje = msg; } public void run() { for (int i=1;i<100;i++) System.out.println( mensaje + i ); } } public class Ejercicio { public static void main(String arg[]) { Habla h = new Habla( “Hola” ) ; Habla c = new Habla( “Chao” ); h.start(); c.start(); } } public class Ejercicio { public static void main(String arg[]) { Thread h = new Thread( new Habla( “Hola” ) ) ; Thread c = new Thread( new Habla( “Chao” ) ); h.start(); c.start(); } } Franco Guidi Polanco 09-03-2007 18 19 Franco Guidi Polanco 09-03-2007 20 Métodos útiles de la clase Thread: sleep Detiene la ejecución del thread actual por la cantidad de milisegundos indicada como parámetro. public static void sleep(long millis) throws InterruptedException Ejemplo: Equivalente: public class MyThread extends Thread { ... public void run() { ... try{ sleep( 1000 ); }catch(InterruptedException e){ //Interrupción } ... } } Franco Guidi Polanco 09-03-2007 Thread.sleep( 1000 ); NOTA: Si en vez de extender Thread se hubiese implementado Runnable, entonces aquí hubiese sido necesaria la referencia a la clase Thread para invocar el método sleep(). 21 Métodos útiles de la clase Thread: sleep Ejemplo Métodos útiles de la clase Thread: sleep Ejemplo public class Hola extends Thread { public void run() { for (int i=1;i<10;i++){ System.out.println( "Hola" + i ); try{ Thread.sleep( 1000 ); }catch(InterruptedException e){} } } } public class Chao extends Thread { public void run() { for (int i=1;i<10;i++){ System.out.println( "Chao" + i ); try{ sleep( (long) ( Math.random() * 1000) ); }catch (InterruptedException e){} } } } Franco Guidi Polanco Detención de 1000 ms (1 segundo) Tiempo de detención al azar 09-03-2007 22 Métodos útiles de la clase Thread: yield Detiene la ejecución del thread actual y permite a otros threads ser ejecutados public static void yield() Ejemplo: Equivalente: public class MyThread extends Thread { ... public void run() { ... yield(); ... } } Franco Guidi Polanco 09-03-2007 23 Franco Guidi Polanco 09-03-2007 Thread.yield(); NOTA: Si en vez de extender Thread se hubiese implementado Runnable, entonces aquí hubiese sido necesaria la referencia a la clase Thread para invocar el método yield(). 24 Métodos útiles de la clase Thread: yield (Ejemplo) Métodos útiles de la clase Thread: yield (Ejemplo) public class Hola extends Thread { public void run() { for (int i=1;i<10;i++){ System.out.println( "Hola" + i ); Thread.yield(); } } } public class Chao extends Thread { public void run() { for (int i=1;i<10;i++){ System.out.println( "Chao" + i ); yield(); } } } Franco Guidi Polanco 09-03-2007 25 Ciclo de vida de un thread Ejecutándose (Running) yield Nuevo thread (New Thread) start Ejecutable (Runnable) Franco Guidi Polanco 09-03-2007 26 Ciclo de vida de un thread (cont.) Inicio de un thread - el thread se ha creado (“Nuevo thread”) y se invoca el método start. El thread pasa al estado “ejecutable”. Transición del estado “ejecutable” al estado “no ejecutable”- ocurre por uno de estos tres motivos: sleep wait bloqueado en I/O No ejecutable (Not Runnable) El thread invoca el método sleep El thread invoca el método wait (*) El thread se bloquea en una operación de I/O finaliza el método run Muerto (Dead) Franco Guidi Polanco 09-03-2007 Tomado de “The Java Tutorial” Sun Microsystems 27 (*) será estudiado más adelante Franco Guidi Polanco 09-03-2007 28 Ciclo de vida de un thread (cont.) Ciclo de vida de un thread (cont.) Transición del estado “no ejecutable” a “ejecutable”: si el thread había invocado el método sleep, el número de milisegundos de pausa ha transcurrido si el thread había invocado el método wait de un objeto, otro thread lo notifica de continuar por llamando al método notify o al método notifyAll del mismo objeto (*) si el thread estaba bloqueado en operación de I/O, la operación se ha completado (*) serán estudiados más adelante Franco Guidi Polanco 09-03-2007 29 Transición del estado “ejecutable” al estado “muerto”: ocurre cuando el método run llega a su fin. El método isAlive ayuda a conocer el estado de un thread: Retorna true si el thread ha sido lanzado (método start invocado) y no detenido Retorna false si el thread está en el estado “Nuevo thread” (no ha sido lanzado) o está “muerto” (método run terminado) Franco Guidi Polanco Ejecución de threads En un sistema con múltiples CPU, cada CPU podría ejecutar un thread distinto Concurrencia Si no es posible el paralelismo, una CPU es responsable de ejecutar múltiples threads Thread A Thread B Thread A Thread A Thread B Thread B Thread A Thread B Thread A ... ... ... CPU CPU CPU 09-03-2007 30 Prioridades de los threads Paralelismo Franco Guidi Polanco 09-03-2007 31 La ejecución de múltiples threads en una sola CPU requiere la determinación de una secuencia de ejecución (“scheduling”) Java soporta un algoritmo de secuenciación de threads simple denominado “fixed priority scheduling” Este algoritmo secuencia la ejecución de threads en base a la “prioridad relativa” que les ha sido asignada Franco Guidi Polanco 09-03-2007 32 Prioridades de los threads (cont.) El “fixed priority scheduling” de Java Cuando se crea un nuevo thread, su prioridad relativa es la misma que la del thread que lo creó La prioridad de un thread puede ser cambiada en cualquier momento por medio del método setPriority. Este método recibe un entero que indica el valor de prioridad (valores más altos indican más altas prioridades) La clase Thread declara tres constantes: public static final int MAX_PRIORITY public static final int MIN_PRIORITY public static final int NORM_PRIORITY Franco Guidi Polanco 10 1 5 09-03-2007 33 Threads y la portabilidad de Java: debilidades 09-03-2007 34 La cosa puede ser peor aun: ... distintos sistemas operativos manejan los threads en distinta forma: por ejemplo NT y Solaris tienen diferentes niveles de prioridades (incluso diferentes respecto los que define Java) 09-03-2007 Franco Guidi Polanco Threads y la portabilidad de Java: debilidades (cont.) La responsabilidad de ejecución de los threads es pasada al sistema operativo Pero... Franco Guidi Polanco Entre todos los threads en estado “ejecutable” es escogido el thread con la prioridad más alta Si hay dos threads con la misma prioridad, es escogido uno de ellos en modo round-robin Cuando el thread en ejecución pasa al estado “no ejecutable” o “muerto” otro thread es seleccionado para su ejecución. La ejecución de un thread es interrumpida si otro thread con más alta prioridad se vuelve “ejecutable”. 35 existen sistemas operativos que implementan “time slicing” (subdivisión de tiempo): el sistema operativo asigna una porción de tiempo a la ejecución de cada thread. En este caso la ejecución de un thread es interrumpida no sólo si otro thread con más alta prioridad se vuelve “ejecutable”, sino también cuando su tiempo asignado de ejecución se acaba. (no todos los sistemas operativos implementan time slicing) Franco Guidi Polanco 09-03-2007 36 Threads egoístas Threads egoístas (cont.) Si un sistema operativo no implementa time slicing, y si el thread no sale del estado “ejecutable”, este continuará su ejecución hasta que muera. Mientras tanto, ningún otro thread podrá ser ejecutado. Volvamos el ejemplo inicial: public class Habla extends Thread { String mensaje; public Habla(String msg){ mensaje = msg; } public void run() { for (int i=1;i<100;i++) System.out.println( mensaje + i ); } } Por lo tanto el siguiente programa ejecutará: ambos threads contemporáneamente en un sistema con time slicing (véase su ejecución en ejemplos anteriores) sólo el thread que imprime Hola hasta terminar las 99 impresiones, y luego el thread que imprime Chao, en un sistema que no soporta time slicing public class Ejercicio { public static void main(String arg[]) { Habla h = new Habla( “Hola” ) ; Habla c = new Habla( “Chao” ); h.start(); c.start(); } } En un sistema sin time slicing, este se vuelve un thread egoísta. Franco Guidi Polanco 09-03-2007 37 Franco Guidi Polanco Moraleja sobre el egoísmo y los threads No se debe asumir que la ejecución de una aplicación se hará siempre en un sistema que soporta time slicing. Por lo tanto, se debe incluir adecuadamente invocaciones a los métodos yield, sleep y wait, si los threads no se bloquean en operaciones de I/O. public class Habla extends Thread { String mensaje; public Habla(String msg){ mensaje = msg; } public void run() { for (int i=1;i<100;i++){ System.out.println( mensaje + i ); yield(); } } } Franco Guidi Polanco 09-03-2007 09-03-2007 38 Acceso a datos compartidos Es común que dos o más threads tengan acceso a objetos en común Ejemplo Supongamos una aplicación con dos threads que actualizan un objeto compartido, de la clase Historial: public class Historial { String[] mensajes = new String[1000]; int pos = 0; public void agregar(String msg) { mensaje[pos] = msg; pos++; } } 39 Franco Guidi Polanco 09-03-2007 40 Acceso a datos compartidos (cont.) Acceso a datos compartidos (cont.) Se espera que ocurra lo siguiente: public class Habla extends Thread { String mensaje; Historial historial; public Habla(String msg, Historial h){ mensaje = msg; historial = h; } public void run() { for (int i=1;i<100;i++){ historial.agregar( mensaje ); yield(); } } Thread Hola mensaje[pos]=msg; pos++; mensaje[pos]=msg; pos++; 41 Acceso a datos compartidos (cont.) Thread Chao mensaje[0]=“Hola” mensaje[pos]=msg; pos++; pos++; pos = 2 mensaje[pos]=msg; pos++; mensaje[2]=“Hola” pos = 3 mensaje[pos]=msg; pos++; Franco Guidi Polanco mensaje[3]=“Chao” pos = 4 09-03-2007 42 Bloqueo del objetos compartidos Pero podría ocurrir lo siguiente: mensaje[pos]=msg; Franco Guidi Polanco mensaje[1]=“Chao” pos = 2 mensaje[2]=“Hola” pos = 3 mensaje[pos]=msg; pos++; 09-03-2007 Thread Hola mensaje[0]=“Hola” pos = 1 mensaje[pos]=msg; pos++; public class Ejercicio { public static void main(String arg[]) { Historial historial = new Historial(); Habla h = new Habla( “Hola”, historial ) ; Habla c = new Habla( “Chao”, historial ); h.start(); c.start(); } } Franco Guidi Polanco Thread Chao mensaje[0]=“Chao” pos = 1 mensaje[pos]=msg; mensaje[3]=“Chao” pos++; pos = 4 mensaje[3]=“Hola” La sincronización para el acceso a objetos compartidos se basa en el concepto de “monitor”, desarrollado por C.A.R. Hoare. Un monitor es una porción de código protegida por un “mutex” (“mutual exclusion semaphore”). Sólo un thread puede tener el mutex de un objeto en un momento dado. Si un segundo thread trata de obtener un mutex ya adquirido por otro thread, se bloquea hasta que el primero libere el mutex. Al momento de liberarse un mutex, todos los threads en espera de él se “despertarán”; en base a algún criterio (orden de prioridad, FIFO, etc.) el mutex será dado a uno de ellos. pos = 5 09-03-2007 43 Franco Guidi Polanco 09-03-2007 44 Bloqueo del objetos compartidos: métodos synchronized Bloqueo del objetos compartidos: analogía Un edificio en el cual algunas oficinas tienen llave y otras tiene libre acceso. El monitor es el conjunto de oficinas cuyo acceso requiere la llave. Los threads son las personas que quieren acceder a las oficinas. Para entrar a una oficina con llave, una persona tiene que obtener el manojo con las llaves de la oficina. El manojo con las llaves es el mutex del edificio: solo la persona que tiene el manojo puede ingresar a las oficinas con llave. Las otras son de libre acceso. En Java el bloqueo de un objeto ocurre cuando un thread entra a un método declarado como synchronized (de un objeto compartido). public class Historial { Ejemplo: ... public synchronized void agregar(String msg) { mensaje[pos] = msg; pos++; } } Objeto Al momento de entrar a un método synchronized de un objeto compartido, un thread se encontrará con una de las siguientes situaciones: Mutex Mutex libre: el thread tomará el mutex, ejecutará el método y lo liberará sólo al momento de terminar la ejecución de dicho método. Mutex tomado por otro thread: el thread se bloqueará en espera de que el primero lo libere. Threads Franco Guidi Polanco 09-03-2007 45 Bloqueo del objetos compartidos: métodos synchronized (cont.) Franco Guidi Polanco 09-03-2007 46 Bloqueo del objetos compartidos: métodos synchronized (cont.) Consecuencia: sólo un thread a la vez podrá ejecutar un método synchronized sobre un objeto. Al interrumpirse la ejecución de un thread que accede a un método synchronized, el paso se dará a otro thread que no requiera el mutex sobre tal objeto (i.e. que no esté solicitando la ejecución de cualquiera de sus métodos synchronized). Una vez liberado el mutex, los threads bloquados en espera de él se vuelven “ejecutables”. Objeto En el caso de nuestro ejemplo, mientras un thread se encuentra ejecutando el método agregar, ningún otro thread puede ejecutar dicho método (pues la ejecución de agregar requiere el mutex del objeto historial). Thread Hola mutex historial.agregar() Thread Chao historial.agregar() historial mensaje[pos]=msg; pos++; Franco Guidi Polanco 09-03-2007 47 Franco Guidi Polanco 09-03-2007 48 Bloqueo del objetos compartidos: métodos synchronized (cont.) Métodos synchronized: ejemplo La invocación a sleep dentro de un thread no libera el mutex de los objetos que eventualmente pudiera tener en su poder. Zzz En el ejemplo de la clase Historial, el método agregar requiere ser synchronized, en cambio getCapacidad no lo requiere: public class Historial { String[] mensajes = new String[1000]; int pos = 0; public synchronized void agregar(String msg) { mensaje[pos] = msg; pos++; } public int getCapacidad(){ return mensajes.length; } z Objeto } Franco Guidi Polanco 09-03-2007 49 En la clase Historial, ¿el método que retorna el número de elementos ingresados al arreglo debe ser synchronized? public class Historial { String[] mensajes = new String[1000]; int pos = 0; public synchronized void agregar(String msg) { mensaje[pos] = msg; pos++; } public int getCapacidad(){ return mensajes.length; } ¿? public synchronized int getElementos() { return pos; } } 09-03-2007 09-03-2007 50 ¿Cuándo deben declararse métodos synchronized? Métodos synchronized Franco Guidi Polanco Franco Guidi Polanco En actualizaciones sobre variables de instancia de objetos que no sean operaciones atómicas: actualización de dos o más variables actualización de variables long, double ¿otros? El costo de declarar métodos synchronized es: mayor lentitud de la ejecución de métodos (por la adquisición del mutex) peligro de deadlock: bloqueo mutuo de dos threads que esperan adquirir mutex intercambiados 51 Franco Guidi Polanco 09-03-2007 52 Ejemplo de deadlock Ejemplo de deadlock (cont.) Dadas las clases Batman y Robin: … y una aplicación que instancia dichas clases y las usa como objetos compartidos por diferentes threads: public class Batman { Robin robin; public void setAsistente( Robin robin ){ this.robin = robin; } public synchronized void vuelveABaticueva(){ robin.subeAlBatimovil(); } public synchronized usaBatiboomerang(){ // usa el batiboomerang como arma de defensa } } public class CiudadGotica { public static void main( String[] arg){ Batman batman = new Batman(); Robin robin = new Robin(); batman.setAyudante( robin ); robin.setjefe( batman ); // aquí se inician distintas operaciones con threads, // entre ellos un thread Alfred y otro thread Pingüino public class Robin { Batman batman; public void setJefe( Batman batman ){ this.batman = batman; } public synchronized powTonkPafBonk(){ batman.usaBatiboomerang(); } } Franco Guidi Polanco 09-03-2007 } } NOTA: Ejemplo adaptado de “Programming Java Threads in the real world” (Parte 2), Allan Holub, disponible on-line: http://www.javaworld.com 53 Franco Guidi Polanco Ejemplo de deadlock (cont.) 3. 1.Un thread llamado Alfred invoca el método vuelveABaticueva() sobre el objeto batman. Alfred obtiene el mutex de batman pero justo antes de que este método invoque el método subeAlBatimovil() de robin, su ejecución es interrumpida. Franco Guidi Polanco Entonces el thread Alfred es reactivado, y trata de invocar subeAlBatimovil() sobre robin. Para invocarlo, sin embargo, debe adquirir el mutex de robin que está en poder del thread Pingüino. (Clase Batman) public synchronized void vuelveABaticueva(){ robin.subeAlBatimovil(); } 4. 2.Otro thread, llamado Pingüino, invoca el método powTonkPafBonk() de robin. El thread Pingüino obtiene el mutex de robin, y trata de ejecutar la instrucción batman.usaBatiboomerang(). Para lograrlo debe adquirir también el mutex de batman, pero como éste está en poder del thread Alfred, se bloquea en espera de su liberación. (Clase Robin) 54 Ejemplo de deadlock (cont.) Imagine que en el ejemplo anterior ocurre lo siguiente: (Clase Batman) 09-03-2007 public synchronized void vuelveABaticueva(){ robin.subeAlBatimovil(); } A este punto el thread Pingüino no puede reactivarse porque no puede obtener el mutex de batman (lo tiene el thread Alfred), ni tampoco el thread Alfred puede hacerlo, porque no puede obtener el mutex de robin (lo tiene el thread Pingüino)… ¡Deadlock! public synchronized powTonkPafBonk(){ batman.usaBatiboomerang(); } 09-03-2007 55 Franco Guidi Polanco 09-03-2007 56 Bloques synchronized Re-adquisición del mutex En Java un thread puede re-obtener el mutex de un objeto que éste ya tiene. public class NoIncurroEnAutoDeadlock { public synchronized void a(){ b(); } public synchronized void b(){ System.out.println( “Estoy en b”); } } synchronized( objeto ){ // instrucciones } Esto evita que un thread incurra en deadlock por culpa de él mismo. Franco Guidi Polanco 09-03-2007 57 Bloques synchronized: ejemplo public class Robot{ Motor motor = new Motor(); ... public void avanzar(){ if( encendido() ) synchronized( motor ){ avanzando = true; motor.aplicarPotencia( 10 ); ... } else preguntar( “Desea encender”, “Si”, “No”); ... } } 09-03-2007 Un thread, al ingresar a un bloque synchronized se bloquea en espera de la adquisición del mutex asociado al objeto (o arreglo) declarado en su encabezado. El mutex es liberado a la salida del bloque. Franco Guidi Polanco 09-03-2007 58 Bloques synchronized: ejemplo En el siguiente ejemplo, el método avanzar adquiere el mutex de motor para ejecutar algunas instrucciones: Franco Guidi Polanco Es posible declarar como synchronized porciones de código dentro de un método de una clase. Esto permite implementar exclusión mutua sobre bloques de instrucciones. Formato: 59 Consecuencia: dado que en el bloque synchronized es adquirido el mutex de motor, ningún otro thread que requiera dicho mutex podrá ser ejecutado concurrentemente. En consecuencia se obtiene un acceso con exclusión mutua sobre la instancia de Motor, aun cuando esta clase no tenga ningún bloque ni método synchronized. Franco Guidi Polanco 09-03-2007 60 Bloques y métodos synchronized Bloques synchronized (cont.) El objeto asociado al bloque synchronized puede ser la misma instancia sobre la cual se ejecuta el método (referencia this). Como consecuencia, las siguientes implementaciones son equivalentes: public class MiClase{ public synchronized void miMetodo(){ // Instrucciones } } public class MiClase{ public void miMetodo(){ synchronized( this ){ // Instrucciones } } } Franco Guidi Polanco 09-03-2007 61 Se pueden crear exclusiones en/entre métodos específicos: public class Acta{ double[] notasCatedra = new double[10]; double[] notasAyudantia = new double[10]; public void actualizarCatedra(){ synchronized( notasCatedra ); // Instrucciones } } public void imprimirCatedra(){ synchronized( notasCatedra ); // Instrucciones } } public void actualizarAyudantia(){ synchronized( notasAyudantia ); // Instrucciones } public void imprimirAyudantia(){ synchronized( notasAyudantia ); // Instrucciones } } } Franco Guidi Polanco Sincronización de threads Un thread (Productor) genera un elemento que es agregado a un depósito, este elemento es consumido por otro thread (Consumidor) El depósito tiene capacidad limitada, cuando está lleno, el Productor debe esperar que se disponga de espacio nuevamente. Por su parte el consumidor debe esperar que haya elementos para poder retirarlos. 09-03-2007 Excluyentes 09-03-2007 62 Sincronización de threads El problema consiste en lograr que un thread actúe sólo cuando otro ha concluido cierta actividad (y viceversa): threads mutuamente excluyentes. Problema del productor/consumidor: Franco Guidi Polanco Excluyentes 63 Supongamos que la capacidad del depósito es igual a 1: Productor Depósito Consumidor El problema es más complejo que en los ejemplos anteriores: el depósito no sólo debe soportar acceso concurrente, sino que los threads deben también actuar sincronizadamente. Franco Guidi Polanco 09-03-2007 64 Sincronización de threads: una solución simplista Ejemplo de sincronización Supongamos la siguiente implementación de un Productor y un Consumidor: public class Productor extends Thread { private Deposito deposito; public Productor(Deposito d) { deposito = d; } public void run() { for (int i=1;i<20 ;i++ ) deposito.guardar(); } } public class Consumidor extends Thread{ private Deposito deposito; public Consumidor(Deposito d) { deposito = d; } public void run() { for (int i=1;i<20 ;i++ ) deposito.sacar(); } } Ambos actúan sobre un objeto compartido de la clase Deposito. Franco Guidi Polanco 09-03-2007 65 Sincronización de threads: una solución simplista (cont.) Problema: alto consumo de recursos (CPU) en procesos improductivos. Solución más adecuada: detener los threads hasta que se den las condiciones para que actúen. Una solución simple sería que el productor verificara cada vez si hay espacio en el depósito, y si lo hay, entonces agregara un elemento a él. Lo mismo podría hacer el consumidor antes de intentar sacar un elemento del depósito. public class Deposito{ private int elementos = 0; public synchronized void guardar() { if( elementos = 0 ) elementos++; return; } public synchronized void sacar() { if( elementos > 0 ) elementos--; return; } } Franco Guidi Polanco 09-03-2007 66 Sincronización de threads: uso de métodos wait y notify La clase Object provee el método wait() que detiene un thread hasta que le sea notificada la posibilidad de continuar. El método wait debe ser invocado sobre un objeto compartido por los threads a sincronizar (ej. el depósito) Para poder invocar el método wait es necesario que el thread tenga el mutex del objeto compartido La invocación de wait detiene el thread, lo pone en una lista de espera asociada al objeto, y libera su mutex. Objeto Lista de espera CPU Franco Guidi Polanco 09-03-2007 wait 67 Franco Guidi Polanco 09-03-2007 68 Sincronización de threads: uso de métodos wait y notify (cont.) El thread saldrá de la lista de espera cuando otro thread invoque el método notify sobre el objeto compartido. Al salir de la lista de espera, se bloqueará en espera del mutex del objeto para continuar su ejecución. Una vez re-obtenido el mutex del objeto, el thread que salió de la lista de espera continuará la ejecución del método en la instrucción siguiente al llamado a wait. Si hay más de un thread en la lista de espera, notify reactivará sólo uno de ellos. El criterio de selección del thread a re-activar depende de la implementación de Java. Nota: El método wait puede generar una InterruptedException. Franco Guidi Polanco 09-03-2007 69 Sincronización de threads: uso del método notifyAll Lista de espera llegué llegué Pedro llegué Inscripción para notificación (wait) Franco Guidi Polanco 09-03-2007 Notar que en este modelo la notificación es indirecta: el thread que invoca notify no tiene ninguna referencia al thread que está en espera. La notificación actúa sobre un objeto compartido, y al ser este notificado, un thread en espera es Lista reactivado. de espera llegué llegué llegó Pedro Pedro Pedro Inscripción para notificación (wait) Franco Guidi Polanco 09-03-2007 70 Ejemplo de sincronización (cont.) El método notifyAll permite reactivar todos los threads bloqueados en la lista de espera de un objeto, esto es, se vuelven todos ejecutables. Notar sin embargo que ellos podrán tomar el mutex sólo de uno a la vez. llegué Sincronización de threads: uso de métodos wait y notify (cont.) 71 El depósito está implementado de la siguiente forma: public class Deposito{ private int elementos = 0; public synchronized void guardar() { try{ if( elementos > 0) // Más adelante se verá que no está correcto this.wait(); } catch( InterruptedException e ){} elementos++; System.out.println( "Guardar - numero elementos: " + elementos ); this.notify(); } public synchronized void sacar() { try{ if( elementos == 0) // Más adelante se verá que no está correcto this.wait(); } catch( InterruptedException e ){} elementos--; System.out.println( "Sacar - numero elementos: " + elementos ); this.notify(); } } Franco Guidi Polanco 09-03-2007 72 Ejemplo de sincronización (cont.) Ejemplo de sincronización (cont.) La aplicación crea dos threads (Productor y Consumidor), que accesan el objeto compartido (depósito): public class EjemploProductorConsumidor{ public static void main( String[] arg ) { Deposito deposito = new Deposito(); Productor productor = new Productor( deposito ); Consumidor consumidor = new Consumidor( deposito ); productor.start(); consumidor.start(); } } Franco Guidi Polanco 09-03-2007 73 Ejemplo de sincronización (cont.) Se bloquea en espera del mutex de depósito Mutex Lista de espera depósito Thread Consumidor Thread Productor Verifica que depósito esté vacío y agrega un elemento. Adquiere el mutex de depósito Verifica que depósito tenga elementos. Dado que no los tiene invoca wait del objeto depósito. La invocación de wait libera el mutex de depósito y el thread se agrega a la lista de espera (de depósito). 09-03-2007 74 75 Mutex Lista de espera Thread Consumidor depósito Invoca notify de depósito. Termina la ejecución del método y libera el mutex. Se bloquea en espera del mutex de depósito Sale de la lista de espera y se bloquea en espera del mutex de depósito. Adquiere el mutex de depósito. Saca el elemento del depósito … Invoca notify de depósito (*) Adquiere el mutex de deposito Franco Guidi Polanco 09-03-2007 Ejemplo de sincronización (cont.) Supongamos en un momento cualquiera que el depósito está vacío y se activa el thread Consumidor Thread Productor Franco Guidi Polanco Franco Guidi Polanco (*) El efecto de notify en esta secuencia es nulo, pero ¿qué habría pasado si el mutex de depósito lo hubiera ganado el Productor? 09-03-2007 … 76 Sincronización y bloqueo iterativo (spin lock) En la implementación de Deposito existe aun un problema. Suponga que hay mas de un thread Consumidor, el depósito está vacío, y se da la siguiente secuencia de eventos: 1. Un thread Consumidor toma el mutex del depósito, y verifica la existencia de un elemento en él. Dado que el depósito está vacío, invoca el método wait (sobre el depósito), se bloquea en la lista de espera (de depósito), y libera su mutex. if( elementos == 0) this.wait(); 2. El thread Productor es reactivado, adquiere el mutex de depósito, comprueba que éste está vacío, le agrega un elemento, e invoca notify (sobre el objeto depósito). Franco Guidi Polanco 09-03-2007 77 Sincronización y bloqueo iterativo (spin lock) Este problema nace del hecho que la especificación de Java no establece que la salida de la lista de espera y adquisición del mutex sean implementados como una operación atómica (distintas JVM se pueden comportar de distinto modo). Problemas análogos se presentan cuando: Hay más de un Productor. La notificación ocurre con notifyAll en vez de notify. Franco Guidi Polanco 09-03-2007 79 Sincronización y bloqueo iterativo (spin lock) 3. 4. 5. 6. El thread Consumidor que estaba en la lista de espera de depósito es notificado (sacado de dicha lista), y puesto en espera del mutex (de depósito). El thread Productor libera el mutex de depósito. Otro thread Consumidor, que no estaba en la lista de espera de depósito, adquiere su mutex, comprueba que el depósito tiene un elemento, lo saca del depósito, invoca notify (no interesa que ocurre con esto), y libera el mutex de depósito. El primer thread Consumidor (que en el paso 3 había sido sacado de la lista de espera y bloqueado en espera del mutex) adquiere el mutex de depósito (antes de que el thread Productor trate de agregar un elemento), y continúa su ejecución en el punto en que estaba: trata de sacar un elemento, pero el depósito está vacío: ERROR. Franco Guidi Polanco 09-03-2007 78 Sincronización y bloqueo iterativo (spin lock) Solución al problema anterior: reemplazar la estructura: if( condición de detención ) wait(); por: while( condición de detención ) wait(); Franco Guidi Polanco 09-03-2007 Spin lock 80 Para saber más... Sincronización y bloqueo iterativo (spin lock) The Java Tutorial (Sun Microsystems) Es decir, la clase Deposito queda: http://java.sun.com public class Deposito{ private int elementos = 0; public synchronized void guardar() { try{ while( elementos > 0) this.wait(); } catch( InterruptedException e ){} elementos++; System.out.println( "Guardar - numero elementos: " + elementos ); this.notify(); } public synchronized void sacar() { try{ while( elementos == 0) this.wait(); } catch( InterruptedException e ){} elementos--; System.out.println( "Sacar - numero elementos: " + elementos ); this.notify(); } } Franco Guidi Polanco 09-03-2007 The Java API (Sun Microsystems) http://java.sun.com “Programming Java Threads in the Real World” (Parts 1-9) Allan Hollub. http://www.javaworld.com 81 Franco Guidi Polanco 09-03-2007 82