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