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 eitherObject#notify()
orObject#noifyAll()
is calledvoid wait(long timeout)
- waits for either the milliseconds that are specified to elapse or notify is calledvoid 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 objectwait
was called onvoid 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 obtained via its constructor. Its over-ridden 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.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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 its queue with the max number of even numbers to be generated. Finally the NumberConsumer
class is instantiated twice and launched 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, it's 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.