The producer-consumer pattern in Java 5: using blocking queues in preference to wait()/notify()
A common use for the wait/notify mechanism
is to implement what is sometimes called a producer-consumer pattern.
What is meant by this is that one thread "produces" work that another thread, or various
other threads, then carry out at a convenient moment. Examples of this pattern include:
- a messaging thread logs messages "passed" to it from other threads;
- worker threads of a web server "notify" a statistics thread to update some central
statistics on each request.
A typical case for using the pattern is thus to separate tasks with
different priorities. Logging, for example, can be a relatively expensive
operation and we may not want it to delay completing another operation. By delegating
logging to another thread, we can effectively allow logging to take place at a future
moment when "there's nothing better to do".
The producer-consumer pattern works by having some queue
of pending tasks. The producer places tasks in the list; the consumer removes them.
Both parties use suitable synchronization.
Producer-consumer before Java 5: using a List with wait/notify
Pre Java 5, the common way to implement a producer-consumer pattern was to
use a plain old LinkedList with explicit synchronization.
When we add a "job" to the list, we call notify(); in another thread,
the consumer is sitting waiting for the job to come in. So the
code would look something like this:
public class LoggingThread extends Thread {
private LinkedList linesToLog = new LinkedList();
private volatile boolean terminateRequested;
public void run() {
try {
while (!terminateRequested) {
String line;
synchronized (linesToLog) {
while (linesToLog.isEmpty())
linesToLog.wait();
line = (String) linesToLog.removeFirst();
}
doLogLine(line);
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
private void doLogLine(String line) {
// ... write to wherever
}
public void log(String line) {
synchronized (linesToLog) {
linesToLog.add(line);
linesToLog.notify();
}
}
}
The code is a little messy because we have no explicit queue object: we
just use an everyday list with code around it to perform the queuing. The
queuing code might get more complex, for example, if we wanted to limit the
number of items that could be queued, or if we wanted to prioritise
items in the queue rather than having a simple first-in-first-out policy.
The wait/notify mechanism also provides us no means of imposing fairness:
that is, if two threads want to add a logging while the list is locked (because a line
is being logged from it), which line gets logged first is essentially random.
In the case of logging, this may not seem such a big deal (though in rare
debugging cases could complicate things if you don't know "what happened first").
But in other cases it could matter more.
The Java 5 producer-consumer pattern
Java 5 improves the producer-consumer pattern by providing explicit
blocking queue classes. A blocking queue effectively takes the
place of the list in the code above, and also handles the associated
synchronization, waiting and notifying (though under the hood, these new classes
use the Java 5 lock
features rather than a "raw" wait/notify).
On the next page, we continue by looking at Java blocking queues.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.