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 el notifyAll()
  • 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

  1. Decidir qué datos encapsular en el monitor
  2. Construir un monitor teórico, utilizando tantas variables de condición como sean necesarias
  3. Usar la señalización SC en el monitor teórico
  4. Implementar en Java
    1. Escribir un método synchronized por cada procedimiento
    2. Hacer los datos encapsulados private
    3. Sustituir cada wait(variable_condicion) pos una condición de guarda.
    4. Sustituir cada send(variable_condicion) por una llamada a notifyAll()
    5. Escribir el código de inicialización del monitor en el constructor del mismo

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();
    }
}