Skip to content

8 Condition

Condition

Un cerrojo puede tener asociados una o más condiciones, también llamadas colas de condición (condition queues) o variables de condición (condition variables). Las condiciones son definidas por la interfaz Condition y proporcionan un mecanismo para suspender a un hilo que haya obtenido el cerrojo al que está asociado la condición, porque ésta no se cumpla, liberando el cerrojo para que otro hilo lo pueda adquirir.

Cuando otro hilo que haya adquirido el cerrojo genere un evento por el que sospeche que la condición por la que han sido suspendidos otros hilos pueda cumplirse, podrá notificarlo para que sean reactivados uno o más hilos, de manera que se pongan a la cola de adquisición del cerrojo y, una vez adquirido, volver a evaluar la condición (bucle).

Este mecanismo es similar al que habíamos utilizado con los bloques synchronized y los métodos wait() y notify(). Sin embargo, para un cerrojo podremos determinar distintas condiciones de suspensión independientes con distintas "colas de espera", a diferencia de los bloques synchronized que poseen una única cola de espera de eventos (una única condición).

Para crear una condición de un cerrojo utilizaremos el método factoría newCondition()de la interfaz Lock, que retornará un objeto Condition asociado a dicho cerrojo y que normalmente almacenaremos en una variable cuyo nombre refleje la condición por la que un hilo deberá esperar. Por ejemplo:

Lock lock = new ReentrantLock();
Condition notFull  = lock.newCondition(); 
Condition notEmpty = lock.newCondition();

Si queremos que un hilo sea suspendido en la cola correspondiente a una determinada condición usaremos el método await() de dicho objeto Condition. El método await() tiene una funcionalidad similar al método wait() de los cerrojos intrínsecos usados por synchronized.

lock.lock();
try {
    while (items.length == CAPACITY) {
        notFull.await();
    }
    // ...
} finally {
    lock.unlock();
}

El método await(timeout, timeUnit) se encuentra sobrecargado para que podamos especificar un tiempo máximo que un hilo puede estar suspendido esperando por una determinada condición, transcurrido el cual el hilo será reactivado automáticamente, retornando el valor false. En caso contrario retorna true.

Como la mayoría de métodos bloqueantes, los método await() y await(timeout, timeUnit) lanzan la excepción InterruptedException si el hilo correspondiente es interrumpido mientras está suspendido en espera en una condición, o si ya había sido marcado como interrumpido antes de llamar a await() o await(timeout, timeUnit), reactivando inmediatamente el hilo en ambos casos.

La interfaz Condition nos ofrece otra alternativa, no disponible en la clase Object y los cerrojos intrínsecos, consistente en el método awaitUninterruptedly(), que funciona de forma similar a await(), pero si el hilo es marcado para interrupción mientras está suspendido en la condición, o ya había sido marcado para interrupción antes de llamar a awaitUninterruptedly(), el hilo no será reactivado inmediatamente ni se lanzará la excepción InterruptedException.

Si se llama a await(), await(timeout, timeUnit) o awaitUninterruptedly() sobre una condición de un cerrojo sin estar en posesión de dicho cerrojo, se lanzará la excepción IllegalMonitorStateException, ya que la condición está asociada a la posesión del cerrojo correspondiente.

Para reactivar un hilo que ha sido bloqueado mediante await() o await(timeout, timeUnit) o awaitUninterruptedly() en una determinada condición, otro hilo debe llamar al método signal() o al método signalAll() de dicha condición, estando en posesión del cerrojo correspondiente, para notificar que se ha producido algún "evento" que hace que sea posible que dicha condición se cumpla.

Como consecuencia de la llamada a signal() el primer los hilos que estuvieran esperando en la cola de dicha condición al haber llamado a await(), await(timeout, timeUnit) o awaitUninterruptedly() en dicha condición será reactivado, colocándose en la cola de adquisición del cerrojo, de manera que una vez adquirido vuelva a comprar la condición (bucle).

Si usamos signalAll() para la notificación, entonces será reactivados todos los hilos que estuvieran esperando en dicha condición, siendo añadido a la cola de adquisición del cerrojo.

Un aspecto importante es que se debe hacer signal() o signalAll()sobre la condición relacionada con el "evento" que se ha producido, y no sobre cualquier condición asociada al cerrojo correspondiente, ya que queremos reactivar a los hilos adecuados, aquellos para los que el "evento" puede ser relevante.

Condition

Figura 7 - Condition

Al ser reactivados debemos tener en cuenta que los hilos deben volver a adquirir el cerrojo, lo que puede implicar que para cuando un hilo reactivado consiga adquirir el cerrojo para continuar con la ejecución de su sección crítica la condición que lo bloqueaba vuelva a dejar de cumplirse, a pesar de que en algún momento sí se cumplió y otro hilo lo notificó. Por este motivo se debe evaluar la condición en un bucle while y no en un simple if.

lock.lock();
try {
    while (items.length == CAPACITY) {
        notFull.await();
    }
    // ...
} finally {
    lock.unlock();
}

Nunca hagas esto

Nunca uses los métodos wait(), notify y notifyAll del objeto Condition

Algo que debemos tener en cuenta es que, dado que Condition extiende de Object, nuestro objeto Condition también posee los métodos wait(), notify y notifyAll, pero nunca debemos usarlos. Estos métodos sólo deben ser usados desde bloques o métodos synchronized.

Si creamos condiciones en un Lock en el que ha establecido el modo justo, cuando los hilos sean reactivados mediante una notificación, se añadirán "en orden" a la cola de adquisición del cerrojo.

Proyecto Condition

En este proyecto realizaremos el ejemplo del productor-consumidor que vimos en un apartado anterior, pero usando los cerrojos y las condiciones en vez de bloques synchronized.

public class Main {

    public static void main(String[] args) {
        Bakery bakery = new Bakery();
        Thread doughnutProducerThread = new Thread(new DoughnutProducer(bakery), "Doughnut producer");
        Thread doughnutConsumerThread = new Thread(new DoughnutConsumer(bakery), "Doughnut consumer");
        doughnutProducerThread.start();
        doughnutConsumerThread.start();
    }

}
import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Bakery {

    private static final int TRAY_CAPACITY = 10;

    private final ArrayList<Integer> tray = new ArrayList<>();
    private final ReentrantLock lock = new ReentrantLock(true);
    private final Condition isNotFull = lock.newCondition();
    private final Condition isNotEmpty = lock.newCondition();

    public void addToTray(Integer doughnut) throws InterruptedException {
        lock.lock();
        try {
            while (tray.size() >= TRAY_CAPACITY) {
                System.out.println("Producer waiting for the tray to have room");
                isNotFull.await();
            }
            tray.add(doughnut);
            System.out.printf("Producer puts doughnut #%d on the tray\n", doughnut);
            isNotEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Integer extractFromTray() throws InterruptedException {
        Integer doughnut;
        lock.lock();
        try {
            while (tray.isEmpty()) {
                System.out.println("Consumer waiting for the tray to have a doughnut");
                isNotEmpty.await();
            }
            doughnut = tray.remove(0);
            System.out.printf("Consumer extracts doughnut #%d from tray\n", doughnut);
            isNotFull.signal();
            return doughnut;
        } finally {
            lock.unlock();
        }
    }

}
import java.util.Objects;
import java.util.concurrent.TimeUnit;

public class DoughnutConsumer implements Runnable {

    private final Bakery bakery;

    public DoughnutConsumer(Bakery bakery) {
        Objects.requireNonNull(bakery);
        this.bakery = bakery;
    }

    @Override
    public void run() {
        Integer doughnut;
        while (!Thread.currentThread().isInterrupted()) {
            try {
                doughnut = bakery.extractFromTray();
            } catch (InterruptedException e) {
                System.out.println("Consumer has been interrupted while extracting from tray");
                return;
            }
            try {
                eat(doughnut);
            } catch (InterruptedException e) {
                System.out.println("Consumer has been interrupted while eating");
                return;
            }
        }
        System.out.println("Consumer has been interrupted");
    }

    private void eat(int doughnut) throws InterruptedException {
        System.out.printf("Consumer is eating doughnut #%d\n", doughnut);
        TimeUnit.SECONDS.sleep(1);
    }

}
import java.util.Objects;
import java.util.concurrent.TimeUnit;

public class DoughnutProducer implements Runnable {

    private final Bakery bakery;
    private int doughnutNumber;

    public DoughnutProducer(Bakery bakery) {
        Objects.requireNonNull(bakery);
        this.bakery = bakery;
    }

    @Override
    public void run() {
        Integer doughnut;
        while (!Thread.currentThread().isInterrupted()) {
            try {
                doughnut = makeDoughnut();
            } catch (InterruptedException e) {
                System.out.println("Producer has been interrupted while making a doughnut");
                return;
            }
            try {
                bakery.addToTray(doughnut);
            } catch (InterruptedException e) {
                System.out.println("Producer has been interrupted while adding a doughnut to the tray");
                return;
            }
        }
        System.out.println("Producer has been interrupted");
    }

    private int makeDoughnut() throws InterruptedException {
        Integer doughnut = ++doughnutNumber;
        System.out.printf("Producer is making doughnut #%d\n", doughnut);
        TimeUnit.SECONDS.sleep(3);
        return doughnut;
    }

}

Si ejecutamos el programa veremos que funciona de manera similar al "Proyecto WaitNotify".