Métodos de objetos de Java: wait & notificar

Este artículo es el tutorial final de una serie que describe los métodos a menudo olvidados de la clase de objeto base del lenguaje Java. Los siguientes son los met...

Introducción

Este artículo es el tutorial final de una serie que describe los métodos a menudo olvidados de la clase de objeto base del lenguaje Java. Los siguientes son los métodos del Objeto base de Java que están presentes en todos los objetos de Java debido a la herencia implícita de Objeto.

El enfoque de este artículo son los métodos Object#wait() y Object#notify (y sus variaciones) que se utilizan para comunicar y coordinar el control entre subprocesos de una aplicación multiproceso.

Resumen básico

El método Object#wait() se usa dentro de un bloque de sincronización o un método miembro y hace que el subproceso al que se llama espere indefinidamente hasta que otro subproceso llame a Object#notify() (o variación Object#notifyAll()) en el mismo objeto en el que se invocó el Object#wait() original.

Espera tiene tres variaciones:

  • void wait() - espera hasta que se llame Object#notify() u Object#noifyAll()
  • void wait(long timeout) - espera a que transcurran los milisegundos especificados o se llama a la notificación
  • void wait(long timeout, int nanos) - igual que el anterior pero con la precisión adicional de los nanosegundos suministrados

El Object#notify() se usa para activar un único subproceso que está esperando un objeto al que se llamó wait. Tenga en cuenta que en el caso de varios subprocesos esperando en el objeto, el sistema operativo selecciona aleatoriamente el subproceso activado.

Notify tiene tres variaciones:

  • void notificar () - selecciona aleatoriamente y activa un hilo que espera en el objeto al que se llamó esperar
  • void notifyAll () - despierta todos los subprocesos que esperan en el objeto

El clásico problema del consumidor productor

Como todas las cosas en la programación, estos conceptos de usar Object#wait() y Object#notify() se entienden mejor a través de un ejemplo cuidadosamente pensado. En este ejemplo, voy a implementar una aplicación de productor/consumidor de subprocesos múltiples para demostrar el uso de esperar y notificar. Esta aplicación usará un productor para generar un número entero aleatorio que representará una cantidad de números aleatorios pares que los subprocesos de consumo necesitarán generar aleatoriamente.

El diseño de la clase y las especificaciones para este ejemplo son los siguientes:

NumberProducer: produce un número entero aleatorio entre 1 y 100 que representa la cantidad de números pares aleatorios que un consumidor necesitará generar. El productor debe colocar el número aleatorio en una cola donde un consumidor puede recuperarlo y comenzar a producir números pares aleatorios.

NumberQueue: una cola que pondrá en cola un número del productor y sacará ese número a un consumidor que espera ansiosamente la oportunidad de generar una serie de números pares aleatorios

NumberConsumer: un consumidor que recuperará un número de la cola que representa el número de enteros pares aleatorios para generar

La ‘Cola de Números’.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.LinkedList;

public class NumberQueue {
    private LinkedList<Integer> numQueue = new LinkedList<>();

    public synchronized void pushNumber(int num) {
        numQueue.addLast(num);
        notifyAll();
    }

    public synchronized int pullNumber() {
        while(numQueue.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return numQueue.removeFirst();
    }

    public synchronized int size() {
        return numQueue.size();
    }
}

NumberQueue tiene una LinkedList que contendrá los datos numéricos internamente y brindará acceso a ellos a través de tres métodos sincronizados. Aquí, los métodos se sincronizan para que se coloque un bloqueo en el acceso a la estructura de datos LinkedList, lo que garantiza que, como máximo, solo un subproceso puede tener control sobre el método a la vez. Además, el método NumberQueue#pushNumber llama a su método Object#notifyAll heredado al agregar un nuevo número para que los consumidores sepan que hay trabajo disponible. De manera similar, el método NumberQueue#pullNumber utiliza un bucle junto con una llamada a su método heredado Object#wait para suspender la ejecución si no tiene números en su lista hasta que tenga datos para los consumidores.

La clase NumberProducer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.Random;

public class NumberProducer extends Thread {
    private int maxNumsInQueue;
    private NumberQueue numsQueue;

    public NumberProducer(int maxNumsInQueue, NumberQueue numsQueue) {
        this.maxNumsInQueue = maxNumsInQueue;
        this.numsQueue = numsQueue;
    }

    @Override
    public void run() {
        System.out.println(getName() + " starting to produce ...");
        Random rand = new Random();
        // continuously produce numbers for queue
        while(true) {
            if (numsQueue.size() < maxNumsInQueue) {
                // random numbers 1-100
                int evenNums = rand.nextInt(99) + 1;
                numsQueue.pushNumber(evenNums);
                System.out.println(getName() + " adding " + evenNums);
            }
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

NumberProducer hereda la clase Thread y contiene un campo llamado maxNumsInQueue que limita la cantidad de elementos que puede contener la cola, y también tiene una referencia a la instancia NumberQueue a través de su campo numsQueue, que gana a través de un solo constructor. Anula el método Thread#run que contiene un bucle infinito que agrega un número entero aleatorio entre 1 y 100 a NumberQueue cada 800 milisegundos. Esto sucede siempre que la cola esté dentro de su límite, llenando así la cola y gobernando el trabajo para los consumidores.

La clase NumberConsumer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.StringJoiner;

public class NumberConsumer extends Thread {
    private NumberQueue numQueue;

    public NumberConsumer(NumberQueue numQueue) {
        this.numQueue = numQueue;
    }

    @Override
    public void run() {
        System.out.println(getName() + " starting to consume ...");
        Random rand = new Random();
        // consume forever
        while(true) {
            int num = numQueue.pullNumber();
            List<Integer> evens = new ArrayList();
            while(evens.size() < num) {
                int randInt = rand.nextInt(999) + 1;
                if (randInt % 2 == 0) {
                    evens.add(randInt);
                }
            }
            String s = "                                 " + getName() + " found " + num + " evens [";
            StringJoiner nums = new StringJoiner(",");
            for (int randInt : evens) {
                nums.add(Integer.toString(randInt));
            }
            s += nums.toString() + "]";
            System.out.println(s);
        }
    }
}

NumberConsumer también hereda de Thread y mantiene una referencia a NumberQueue a través del campo de referencia numQueue obtenido a través de su constructor. Su método de ejecución anulado contiene de manera similar un bucle infinito, que en su interior extrae un número de la cola a medida que están disponibles. Una vez que recibe el número, ingresa a otro ciclo que produce números enteros aleatorios del 1 al 1000, prueba la uniformidad y los agrega a una lista para su visualización posterior.

Una vez que encuentra el número requerido de números pares aleatorios especificados por la variable num extraídos de la cola, sale del ciclo interno y proclama a la consola sus hallazgos.

La clase EvenNumberQueueRunner.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EvenNumberQueueRunner {

    public static void main(String[] args) {
        final int MAX_QUEUE_SIZE = 5;

        NumberQueue queue = new NumberQueue();
        System.out.println("    NumberProducer thread         NumberConsumer threads");
        System.out.println("============================= =============================");

        NumberProducer producer = new NumberProducer(MAX_QUEUE_SIZE, queue);
        producer.start();

        // give producer a head start
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        NumberConsumer consumer1 = new NumberConsumer(queue);
        consumer1.start();

        NumberConsumer consumer2 = new NumberConsumer(queue);
        consumer2.start();
    }
}

EvenNumberQueueRunner es la clase principal de esta aplicación que comienza instanciando la clase NumberProducer y la inicia como un hilo. Luego le da una ventaja de 3 segundos para llenar su cola con el número máximo de números pares que se generarán. Finalmente, la clase NumberConsumer se instancia dos veces y los lanza como subprocesos que luego sacan números de la cola y crean el número indicado de enteros pares.

Aquí se muestra un ejemplo de salida del programa. Tenga en cuenta que no es probable que dos ejecuciones produzcan el mismo resultado, ya que esta aplicación es de naturaleza puramente aleatoria desde los números producidos hasta la aleatoriedad con la que el sistema operativo cambia entre subprocesos activos en la CPU.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    NumberProducer thread         NumberConsumer threads
============================= =============================
Thread-0 starting to produce ...
Thread-0 adding 8
Thread-0 adding 52
Thread-0 adding 79
Thread-0 adding 62
Thread-1 starting to consume ...
Thread-2 starting to consume ...
                                 Thread-1 found 8 evens [890,764,366,20,656,614,86,884]
                                 Thread-2 found 52 evens [462,858,266,190,764,686,36,730,628,916,444,370,860,732,188,652,274,608,912,940,708,542,760,194,642,192,22,36,622,174,66,168,264,472,228,972,18,486,714,244,214,836,206,342,388,832,8,666,946,116,342,62]
                                 Thread-2 found 62 evens [404,378,276,308,470,156,96,174,160,704,44,12,934,426,616,318,942,320,798,696,494,484,856,496,886,828,386,80,350,920,142,686,118,240,398,488,976,512,642,108,542,122,536,482,734,430,564,200,844,462,12,124,368,764,496,728,802,836,478,986,292,486]
                                 Thread-1 found 79 evens [910,722,352,656,250,974,602,342,144,952,916,188,286,468,618,496,764,642,506,168,966,274,476,744,142,348,784,164,346,344,48,862,754,896,896,784,574,464,134,192,446,524,424,710,128,756,934,672,816,604,186,18,432,250,466,144,930,914,670,434,764,176,388,534,448,476,598,984,536,920,282,478,754,750,994,60,466,382,208]
Thread-0 adding 73
                                 Thread-2 found 73 evens [798,692,698,280,688,174,528,632,528,278,80,746,790,456,352,280,574,686,392,26,994,144,166,806,750,354,586,140,204,144,664,214,808,214,218,414,230,364,986,736,844,834,826,564,260,684,348,76,390,294,740,550,310,364,460,816,650,358,206,892,264,890,830,206,976,362,564,26,894,764,726,782,122]
Thread-0 adding 29
                                 Thread-1 found 29 evens [274,600,518,222,762,494,754,194,128,354,900,226,120,904,206,838,258,468,114,622,534,122,178,24,332,432,966,712,104]
Thread-0 adding 65

... and on and on ...

Me gustaría tomarme un momento para explicar mi uso del método notifyAll() dentro de NumberQueue#pushNumber porque mi elección no fue aleatoria. Al usar el método notifyAll(), le doy a los dos subprocesos de consumo la misma oportunidad de sacar un número de la cola para trabajar en lugar de dejar que el sistema operativo elija uno sobre el otro. Esto es importante porque si simplemente hubiera usado notify() entonces hay una buena posibilidad de que el subproceso que el sistema operativo seleccione para acceder a la cola aún no esté listo para hacer más trabajo y esté trabajando en su último conjunto de incluso números (bueno, es un poco exagerado que todavía esté tratando de encontrar hasta un máximo de 1000 números pares después de 800 milisegundos, pero espero que entiendas a lo que me refiero). Básicamente, lo que quiero dejar claro aquí es que en casi todos los casos debería preferir el método notifyAll() sobre la variante notify().

Conclusión

En este artículo final de la serie de métodos de clase de objetos de Java, he cubierto el propósito y el uso de las variaciones de esperar y notificar. Debe decirse que estos métodos son bastante primitivos y que los mecanismos de concurrencia de Java han evolucionado desde entonces pero, en mi opinión, “esperar” y “notificar” siguen siendo un valioso conjunto de herramientas para tener en tu cinturón de herramientas de programación Java.

Como siempre, gracias por leer y no se avergüence de comentar o criticar a continuación.

Licensed under CC BY-NC-SA 4.0