Tema 4: Control de la concurrencia en Java (API estándar)
Exclusión mutua entre hilos
En Java, la EM se logra a nivel de objetos. Todo objeto tiene asociado un cerrojo (lock).
Técnicas
- Bloques
synchronized
, el segmento de código que debe ejecutarse en EM se etiqueta asi, es el equivalente a una región crítica en java. - Métodos
synchronized
, aquellas instancias de recursos que deben manejarse bajo EM se encapsulan en una clase y todos sus métodos modificadores se etiquetan con esta palabra.
Por tanto dos o más hilos están en EM cuando llaman a métodos synchronized
de un objeto (de EM) o ejecutan métodos con bloques de código sincronizados.
public metodo_x (parametros) {
//Resto de codigo del metodo. No se ejecuta en EM.
/* comienzo de la seccion critica */
synchronized (object) {
//Bloque de sentencias criticas
//Todo el codigo situado entre llaves se ejecuta bajo EM
}
/* fin de la seccion critica */
}// metodo_x
Son una implementación del concepto de región crítica. Como cerrojo se puede usar el propio objeto this
.
El bloque sólo se ejecuta si se obtiene el cerrojo sobre el objeto. Útil para adecuar código secuencial a un entorno concurrente.
Un método synchronized
fuerza la EM entre todos los hilos que lo invocan, tmabién lo hace con otros métodos synchronized
del objeto.
Cualquier hilo que trate de ejecutar un método sincronizado deberá esperar si otro método sincronizado ya está en ejecución. Los métodos que no sean sincronizados pueden seguir ejecutándose concurrentemente.
El cerrojo está asociado a cada instancia de una clase, los métodos de clase(static) también pueden ser synchronized
.
### Primer protocolo de control de EM
Acceder al recurso compartido donde sea necesario, definir un objeto para control de la exclusión mutua (o usar el this
).
Sincronizar el código crítico utilizando el cerrojo del objeto de control dentro de un bloque synchronized
de instrucciones.
Segundo protocolo de control de EM
Encapsular el recurso crítico en una clase, definiendo todos los métodos como no estáticos y synchronized
. O al menos, todos los modificadores.
Crear hilos que compartan una instancia de la clase creada.
OJO, esto último no provee sincronización.
La posesión del bloqueo es por hilo
Un método sincronizado puede invocar a otro método sincronizado sobre el mismo objeto (REENTRANCIA), permitiendo llamadas recursivas a métodos sincronizados. Permite invocar métodos heredados sincronizados.
Deadlock (Interbloqueo)
Se producen cuando hay condiciones de espera de liberación de bloqueos cruzados entre dos hilos.
Solo pueden evitarse mediante un análisis cuidadoso y riguroso del uso del código sincronizado.
Anidar bloques synchronized
produce interbloqueo.
Sincronización entre hilos
En java la sincronización entre hilos se logra con los métodos wait
, notify
y notifyAll
(clase Object
). Deben ser utilizados únicamente dentro de métodos o código de tipo synchronized
.
Cuando un hilo llama a un método que hace wait
las siguientes acciones se ejecutan atómicamente:
- Hilo llamante queda suspendido y bloqueado
- EM sobre el objeto liberada
- Hilo colocado en una cola única (wait-set) de espera asociado al objeto
Cuando un hilo llama a un método que hace notify, uno de los hilos bloqueados en la cola pasa a listo. Java no especifica cual. Depende de la implementación (JVM). Si se llama al método notifyAll
, todos los hilos de dicha cola son desbloqueados y pasan a listo. Accederá al objeto aquél que sea planificado.
Si el wait-set está vacío, notify
y notifyAll
no tienen efecto.
Condiciones de guarda
No se puede escribir código de hilos asumiendo que un hilo concreto recibirá la notificación, diferentes hilos pueden estar bloqueados sobre un mismo objeto a la espera de diferentes condiciones, pero el wait-set es único.
Dado que el notifyAll()
los despierta a todos de forma incondicional, es posible que reactive hilos para los cuales no cumplen la condición de espera.
Condición de guarda: siempre que el hilo usuario invoca a wait()
, lo primero al despertar es volver a comprobar su condición particular, volviendo al bloqueo si ésta aún no se cumple.
public synchronized void m_receptor_senal() {
// hilo usuario se bloqueara en el metodo a espera de condicion
// el hilo usuario pasa al wait-set
while (!condicion) try {
wait();
} // condicion de guarda
catch ()(Exception e) {}
//codigo accedido en e.m.
}
public synchronized void m_emisor_senal() {
/**
hilo usuario enviara una senal a todos los hilos del waitset
la guarda garantiza que se despiertan los adecuados
codigo en e.m. que cambia las condiciones de estado notiyAll();
*/
}
Monitores en Java
Un monitor es un objeto que implementa acceso bajo EM a todos sus métodos, y provee sincronización. En Java, son objetos de una clase cuyos métodos públicos son todos synchronized
.
Un objeto con métodos synchronized
proporciona un cerrojo único que permite implantar monitores con comodidad y EM bajo control.
Los métodos wait
, notify
y notifyAll
permiten sincronizar los accesos al monitor, de acuerdo as la semántica de los mismos ya conocida.
class Monitor {
//definir aqui datos protegidos por el monitor
public Monitor() {...} //constructor
public synchronized tipo1 metodo1() throws InterruptedException {
...
notifyAll();
...
while (!condicion1) wait();
}
public synchronized tipo2 metodo2() throws InterruptedException {
...
notifyAll();
...
while (!condicion1) wait();
}
}
Semántica
Cuando un método synchronized
del monitor llama a wait()
, libera la EM sobre el monitor y encola al hilo que llamó al método en el wait-set.
Cuando un método synchronized
del monitor hace notify()
, un hilo del wait-set (no se especifica cual) pasará a la cola de hilos que esperan el cerrojo y se renaudará cuando sea planificado.
Cuando otro método del monitor hace notifyAll()
, todos los hilos del wait-set
pasarán a la cola de hilos que esperan el cerrojo y se renaudarán cuando sean planificados.
El monitor de Java no tiene variables de condición, sólo una cola de bloqueo de espera implícita.
La política de señalización es señalar y seguir (SC)
- El método (hilo) señalador sigue su ejecución
- El hilo(s) señalado(s) pasan del wait-set a la cola de procesos que esperan el cerrojo
Para dormir a un hilo a la espera de una condición usamos el método wait()
, dentro de un método synchronized
. Es menos fino que una variable de condición, y el conjunto de espera es único(wait-set)
### Peculiaridades
No es posible programar a los hilos suponiendo que recibirán la señalización cuándo la necesiten. Al no existir variables de condición, sino una única variable implícita, es conveniente usar notifyAll()
para que todos los procesos comprueben la condición que los bloqueó.
No es posible señalar un hilo en concreto, por lo que:
- Es aconsejable bloquear a los hilos en el
wait-set
con una condición de guarda en conjunción con elnotifyAll()
while(!condicion) try{wait();} catch (InterruptedException e) {return;}
- Todos serán despertados, comprobarán la condición y volverán a bloquearse, excepto los que la encuentren verdadera, que pasan a espera del cerrojo sobre el monitor.
No es aconsejable usar un if
en la condición de guarda. Los campos protegidos por el monitor suelen declararse private.
Técnica de diseño en java
- Decidir qué datos encapsular en el monitor
- Construir un monitor teórico, utilizando tantas variables de condición como sean necesarias
- Usar la señalización SC en el monitor teórico
- Implementar en Java
- Escribir un método
synchronized
por cada procedimiento - Hacer los datos encapsulados private
- Sustituir cada
wait(variable_condicion)
pos una condición de guarda. - Sustituir cada
send(variable_condicion)
por una llamada anotifyAll()
- Escribir el código de inicialización del monitor en el constructor del mismo
- Escribir un método
Ejemplo de monitor
class CubbyHole {
private int contents;
private boolean available = false;
public synchronized int get() {
while (available == false) {
try {
wait();
} catch (InterruptedException e) {
}
}
available = false;
notifyAll();
return contents;
}
public synchronized void put(int value) {
while (available == true) {
try {
wait();
} catch (InterruptedException e) {
}
}
contents = value;
available = true;
notifyAll();
}
}