Concurrencia en Java • Herramientas proporcionadas por Java • La Máquina Virtual (JVM) Pedro Pablo Gómez Martín La clase Thread • Clase principal con la que conseguir concurrencia. • La llamada a su método start() ocasiona la ejecución de su método run() en una hebra independiente. • Todas las hebras que se ejecutan en una misma máquina virtual comparten recursos, como por ejemplo la memoria. • La implementación de hebras particulares requiere herencia y sobreescritura del método run(). 2 1 La clase Thread Posible salida class MiHebra extends Thread { int cont; 1 MiHebra(int c) { cont = c; } 1 public void run() { 2 while (true) { 2 System.out.println(cont); } 1 } 1 public static void main(String[] args) { 2 new MiHebra(0).start(); 1 new MiHebra(1).start(); } ... } // class 3 El interfaz Runnable • Requieren la implementación de un método run(). • Se construye una nueva hebra pasando en el constructor un objeto que implemente el interfaz. • La llamada al método start() de la hebra ocasiona la ejecución del método run() del objeto pasado en una nueva hebra. • No requiere herencia de la clase Thread. Se pueden construir objetos que hereden de otras clases y que pueden, no obstante, ser ejecutados. 4 2 El interfaz Runnable class MiHebra implements Runnable { int cont; MiHebra(int c) { cont = c; } public void run() { while (true) { System.out.println(cont); } } public static void main(String[] args) { new Thread(new MiHebra(0)).start(); new Thread(new MiHebra(1)).start(); } } // class 5 Diferencias • El uso de la clase Thread requiere utilizar herencia. Debería usarse solo cuando haya que sobreescribir algún otro método. • El uso del interfaz Runnable permite que la clase herede de cualquier otra (más versátil). Sin embargo, la ejecución requiere la construcción de un objeto añadido (aquel que implementa el interfaz Runnable) además del objeto de la clase Thread. 6 3 Finalización • Cuando se termina la ejecución del método run(). • Cuando llega al método run() una excepción que no se captura. • Cuando se llama al método stop([excepción]) de la hebra (caso particular de la anterior). • Cuando se llama al método destroy() de la hebra. No está implementado. 7 Prioridades • Cada hebra tiene asociada una prioridad que ayuda a la planificador a decidir qué hebra ejecutar. • Existe expropiación entre hebras de igual prioridad • No se garantiza que hebras de prioridades menores pasen a ejecutarse si existe alguna hebra de más prioridad que no está bloqueada. 8 4 Prioridades • La prioridad de una hebra está entre MIN_PRIORITY y MAX_PRIORITY. El valor normal es NORM_PRIORITY, todas definidas como constantes en la clase Thread. • La prioridad inicial de una hebra es la misma que la prioridad de la hebra que la crea. • Puede modificarse en cualquier momento mediante setPriority(int) y consultarse con getPriority() 9 Control del planificador • Además de las prioridades, existen tres métodos estáticos para controlar la planificación: • void sleep(long milis): duerme a la hebra al menos <milis> milisegundos. • void sleep(long milis, int nanos): duerme a la hebra al menos <milis> milisegundos y <nanos> nanosegundos. • void yield(): cede el procesador a otra hebra, pasandose a ejecutar el planificador. • Las dos sleep(...) pueden lanzar la excepción InterruptedException. 10 5 Clases y excepciones Object Thread Throwable Error Class ... Exception RuntimeException ... ... Excepciones de usuario 11 Cerrojos • Todo los objetos (incluidos los arrays) tienen un “cerrojo” (lock). • Solo una hebra puede tener bloqueado el cerrojo de un objeto en un momento dado. Podrá bloquearlo más de una vez antes de liberarlo y solo quedará completamente libre cuando la hebra lo libere tantas veces como lo ha obtenido. • Si una hebra intenta obtener un cerrojo ocupado, quedará suspendida hasta que éste se libere y pueda obtenerlo. • No se puede acceder directamente a los cerrojos. 12 6 Exclusión mútua • Es posible garantizar la ejecución en exclusión mútua de un método definiéndolo como synchronized. • Los métodos synchronized bloquean el cerrojo del objeto actual, o del objeto Class si el método es estático. • Si el cerrojo está ocupado, la hebra se suspende hasta que éste es liberado. • No se ejecutarán simultáneamente dos métodos synchronized de un mismo objeto, pero sí uno que lo sea y cualquier número de otros que no. 13 Exclusión mutua • Pueden sobreescribirse métodos synchronized para que no lo sean en las clases nuevas. Sin embargo sí lo seguirá siendo el método super.metodo(...). • La exclusión mutua es interesante para garantizar la consistencia del estado de los objetos. • Se suelen utilizar en los métodos que hacen que el objeto pase por estados transitorios que no son correctos. Si también se hacen synchronized los métodos para consultar el estado, se evita que puedan verse estados inestables del objeto. 14 7 Exclusión mutua • Puede bloquearse el cerrojo de un objeto dentro de una porción de código mediante synchronized(objeto) { }. • Si el cerrojo está bloqueado por una hebra diferente (ya sea porque está ejecutando un método synchronized o porque está dentro de un bloque como el anterior), la hebra actual se suspenderá. • Pueden usarse estos bloques para clases sin sincronizar que no podamos modificar. Es inseguro, mejor usar herencia. 15 Señalización • Los mecanismos anteriores sirven para evitar la interferencia entre hebras. • Es necesario algún método de comunicación entre ellas. • Todos los objetos implementan los métodos wait() y notify(). Mediante wait() se suspende la hebra actual hasta que alguna otra llame al método notify() del mismo objeto. • Todos los objetos tienen una lista de hebras que están esperando que alguien llame a notify(). 16 8 Señalización Estos métodos están pensados para avisar de cambios en el estado del objeto a hebras que están esperando dichos cambios: synchronized void doWhenCondition() { while(!condition) wait(); /* ... */ } synchronized void changeCondition() { /*...*/ notify(); } 17 Señalización • Tanto el método wait() como el notify() deben ejecutarse dentro de métodos synchronized. • Cuando se llama a wait() se libera el cerrojo del objeto que se tiene bloqueado y se suspende la hebra, de forma atómica. • Cuando se llama a notify() se despierta una de las hebras que estaban esperando. Ésta competirá con cualquier otra por volver a obtener el cerrojo del objeto, sin tener ningún tipo de prioridad. De ahí que sea mejor usar while(!condicion) wait(); 18 9 Señalización - Métodos • wait(long tiempo): espera hasta una notificación, o hasta que pasen <tiempo> milisegundos. Si es 0, la espera es infinita. • wait(long tiempo, int nanos): igual, pero con precisión de nanosegundos. • wait(): igual que wait(0) • notify(): despierta a una única hebra de las que están esperando. • notifyAll(): despierta a todas las hebras. 19 Señalización - Métodos • Todos los métodos anteriores son finales. • Todas las variantes de wait(...) pueden lanzar la excepción InterruptedException. • notify() despierta una hebra cualquiera. No se garantiza que sea la que más tiempo lleva esperando. Es solo interesante cuando se está seguro de que solo habrá una hebra esperando. Es peligroso. • notifyAll() despierta a todas. Es más seguro. Requiere más que nunca el uso de while() en lugar de if(!condition) wait(); 20 10 Interbloqueos • La existencia de varias hebras, y el uso de la exclusión mutua puede ocasionar la aparición de interbloqueos, en el que ninguna de dos hebras puede ejecutarse porque están esperando algo de la otra. • Java no controla el interbloqueo. No se preocupa de detectarlo. Es responsabilidad del diseñador de la aplicación ser cuidadoso para evitar la aparición de interbloqueos. 21 Grupos de hebras • Las hebras se estructuran de forma jerárquica. Cada nodo del árbol es un objeto de la clase ThreadGroup. • Cada ThreadGroup almacena un conjunto de hebras, y otro de ThreadGroup´s que contiene. • Se puede establecer una prioridad al grupo, que actúa como cota máxima de las prioridades de las hebras que contiene. • Cuando alguna de las hebras finaliza por una excepción, se llama al método uncaughtException(...) de su grupo. 22 11 Métodos stop( ) • El método stop() fuerza el lanzamiento de la excepción ThreadDeath. • Es subclase de Error para que no se capture en los catch(Exception e) muy habituales. • Es la única excepción ignorada por el objeto ThreadGroup. • Mediante stop(Throwable) se puede forzar el lanzamiento de cualquier otra excepción. • Los dos métodos están desaconsejados (deprecated) porque pueden dejar objetos en estados inestables. 23 Otros métodos • El método suspend() detiene la hebra hasta que se llama a resume() de la misma hebra. • El método destroy() destruye completamente la hebra, sin liberar ninguno de los cerrojos que pudiera tener bloqueados. No está implementado. • Los tres métodos están desaconsejados (deprecated) porque pueden causar la aparición de exclusión mútua. 24 12 La máquina virtual Por hebra Por JVM Heap Pila de Frames PC Área de Código (text) Tabla de constantes 25 Frames Array de variables locales Pila de operandos Referencia a la tabla de constantes Los long y double ocupan dos entradas. El resto solo una. 26 13 Llamada a métodos • Las instrucciones que llaman a un método toman como parámetro la posición dentro de la tabla de constantes donde está el nombre del método. • Hay cuatro instrucciones distintas para llamar, según el tipo de método (estático, de un interfaz, privados o de superclases, y normales). • Todas miran las características del método para crear un nuevo frame. Además si es synchronized, la hebra pasa a competir por la obtención del cerrojo del objeto. 27 Fin de un método • Existen instrucciones return para todos los tipos básicos y para referencias, que obtienen el valor a devolver de la pila de operandos. • Cuando el método termina, se elimina el frame, el frame anterior pasa a ser el frame actual, y el valor devuelto se apila en la pila de operandos de dicho frame. • Si no hay más frames, la hebra finaliza. • Si el método que finaliza era synchronized, el cerrojo del objeto se libera. 28 14 Fin de un método • Existe la instrucción athrow con la que se lanza una excepción. • Si hay algún manejador de esa excepción, se trata. Si no, se finaliza el método como antes, liberando el cerrojo si el método era synchronized, y lanzando la excepción en el método anterior. • Si no quedan más métodos, se finaliza la hebra, y se llama al método del grupo de la hebra uncaughtException(...). 29 Synchronized(o) • Se implementan usando dos instrucciones de la máquina virtual. Las dos esperan la referencia al objeto cuyo cerrojo se quiere manejar: n n monitorenter: obtiene el cerrojo, o suspende la hebra hasta que lo consiga. monitorexit: libera el cerrojo. • Para un correcto funcionamiento de synchronized(o) se requiere la cooperación del compilador que debe vigilar todas las posibles salidas del bloque. 30 15 Nuevas hebras • No existe una instrucción en la máquina virtual que cree nuevas hebras. • Se utilizan métodos nativos. La mayoría de los métodos de la clase Thread lo son. • La llamada al método start() creará un nuevo motor de ejecución independiente al del que llama al método. La implementación es específica de la plataforma y de la implementación de la máquina virtual para dicha plataforma. • Tampoco hay instrucciones para wait(...), notify() o notifyAll(). 31 Gestión de memoria • Hay una memoria principal, compartida por todas las hebras. • Cada hebra tiene su propio espacio de memoria, con copias locales de variables de la memoria global. • Se requieren mecanismos para sincronizar las copias locales con la memoria principal. • La especificación de la máquina virtual obliga a que se cumplan ciertas normas en las actualizaciones. 32 16 Operaciones atómicas • Memoria principal n n n n read: transmite el contenido de una variable a la memoria local de una hebra. write: almacena el valor transmitido por la memoria local de una hebra en la memoria principal. lock: bloquea un cerrojo. Se ejecuta de forma sincronizada con una hebra. unlock: desbloquea un cerrojo. Se ejecuta de forma sincronizada con una hebra. 33 Operaciones atómicas • Memoria local de una hebra n n load: recoge el valor de un read y lo copia en la memoria local. store: envía el valor de una variable de la memoria local a la memoria principal. • Hebra n n use: transfiere el valor de una variable de la memoria local al motor de ejecución. assign: copia un valor del motor de ejecución a la memoria local de la hebra. 34 17 Operaciones atómicas Thread Memoria de trabajo load x Memoria principal x x store use assign read x Copia maestra write x Motor de ejecución 35 Operaciones atómicas • La especificación de la JVM marca restricciones en el uso de las operaciones. • En general, no se especifica el momento en el que debe actualizarse la memoria principal. Solo se obliga a hacer copia en los lock/unlock. • En principio, todas las operaciones son de 32 bits. Las lecturas/escrituras de double y long se hacen en dos partes. Esto puede original lecturas de valores incorrectos por otras hebras, si obtienen el valor cuando solo se ha realizado una de las operaciones de escritura. 36 18 Variables volatile • Si se desea forzar la escritura en memoria principal de una variable compartida, puede usarse un lock/unlock. • También pueden utilizarse variables volatile. • Las reglas en las operaciones atómicas para estas variables son más estrictas. • Se fuerza que por cada load haya un read, y que por cada store haya un write. • Se trabaja así siempre con el valor de la copia principal. No obstante puede haber problemas en long y double. 37 Bibliografía • The Java Language Specification 2nd edition. • The Java Virtual Machine Specification 2nd edition. • The Java Programming Languaje 2nd edition. • D. Lea, Concurrent Programming in Java. Design Principles and Patterns, Addison Wesley 1996. 38 19