Java's Object Methods: wait & notify

Introduction

This article is the final tutorial of a series describing the often forgotten about methods of the Java language's base Object class. The following are the methods of the base Java Object which are present in all Java objects due to the implicit inheritance of Object.

The focus of this article are the Object#wait() and Object#notify methods (and their variations) which are used to communicate and coordinate control between threads of a multi-threaded application.

Basic Overview

The Object#wait() method is used within a synchronization block or member method and causes the thread it is called in to wait indefinitely until another thread calls Object#notify() (or it's variation Object#notifyAll()) on the same object that the original Object#wait() was called on.

Wait has three variations:

  • void wait() - waits until either Object#notify() or Object#noifyAll() is called
  • void wait(long timeout) - waits for either the milliseconds that are specified to elapse or notify is called
  • void wait(long timeout, int nanos) - same as the one above but, with the extra precision of the nanoseconds supplied

The Object#notify() is used to wake up a single thread that is waiting on an object that wait was called on. Note that in the case of multiple threads waiting on the object the woken up thread is selected randomly by the operating system

Notify has three variations:

  • void notify() - randomly selects and wakes up a thread waiting on the object wait was called on
  • void notifyAll() - wakes up all threads waiting on the object

The Classic Producer Consumer Problem

Like all things in programming, these concepts of using Object#wait() and Object#notify() are best understood through a carefully thought out example. In this example I am going to implement a multi-threaded producer / consumer application to demonstrate the use of wait and notify. This application will use a producer to generate a random integer that is to represent a number of even random numbers that consumer threads will need to randomly generate.

The class design and specifications for this example are as follows:

NumberProducer: produce a random integer between 1-100 that represents the number of random even numbers a consumer will need to generate. The random number is to be placed in a queue by the producer where a consumer can retrieve it and go to work producing random even numbers

NumberQueue: a queue that will enqueue a number from the producer and dequeue that number to a consumer eagerly awaiting the chance to generate a series of random even number

NumberConsumer: a consumer that will retrieve a number from the queue representing the number of random even integers to generate

The NumberQueue.

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 has a LinkedList that will contain the numbers data internally and provide access to it via three synchronized methods. Here the methods are synchronized so that a lock will be placed on the access to the LinkedList data structure guaranteeing that at most only one thread can have control over the method at a time. Furthermore, the NumberQueue#pushNumber method calls it's inherited Object#notifyAll method upon adding a new number letting the consumers know work is available. Similarly, the NumberQueue#pullNumber method uses a loop along with a call to it's inherited Object#wait method to suspend execution if it has no numbers in its list until it has data for consumers.

The NumberProducer class.

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 inherits the Thread class and contains a field called maxNumsInQueue that puts a limit on the number of items the queue can hold, and it also has a reference to the NumberQueue instance via its numsQueue field, which it gains via a single constructor. It overrides the Thread#run method which contains an infinite loop that adds a random integer between 1-100 to the NumberQueue every 800 milliseconds. This happens as long as the queue is within its limit, thus populating the queue and governing work for the consumers.

The NumberConsumer class.

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 also inherits from Thread and maintains a reference to the NumberQueue via the numQueue reference field attained via it's constructor. It's overriden run method similarly contains an infinite loop, which inside it pulls a number off the queue as they are available. Once it receives the number it enters another loop which produces random integers from 1-1000, tests it for evenness and adds them to a list for later display.

Once it finds the required number of random even numbers specified by the num variable pulled off the queue it exits the inner loop and proclaims to the console its findings.

The EvenNumberQueueRunner class.

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 is the main class in this application which begins by instantiating the NumberProducer class and launches it as a thread. Then it gives it a 3 second head start to fill it's queue with the max number of even numbers to be generated. Finally the NumberConsumer class is instantiated twice and launched them as threads which then go off pulling numbers off the queue and creating the indicated number of even integers.

Example output from the program is shown here. Note that no two runs are likely to produce the same output as this application is purely random in nature from the numbers produced to the randomness that the operating system switches between active threads on the CPU.

    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 ...

I would like to take a moment to explain my usage of the notifyAll() method within NumberQueue#pushNumber because my choice was not random. By using the notifyAll() method I am giving the two consumer threads equal chance at pulling a number off the queue to do work on rather than leaving it up to the OS to pick one over the other. This is important because if I had simply used notify() then there is a good chance that the thread the OS selects to access the queue is not yet ready to do more work and is working on it's last set of even numbers (ok, its a little far fetched that it would still be trying to find up to a max of 1000 even numbers after 800 milliseconds but, hopefully you understand what I'm getting at). Basically what I want to make clear here is that in nearly all cases you should prefer the notifyAll() method over the notify() variant.

Conclusion

In this final article of the series of Java Object class methods I have covered the purpose and usage of the variations of wait and notify. It should be said that these methods are fairly primitive and the Java concurrency mechanisms have evolved since then but, in my opinion wait and notify still are a valuable set of tools to have in your Java programming tool belt.

As always, thanks for reading and don't be shy about commenting or critiquing below.

Author image
Lincoln, Nebraska Twitter