transparencias

Anuncio
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
Descargar