CMPU 102 - Assignment 5 Due: by 11:59 PM on Thursday, March 9th

$Revision: 1.6 $

In this assignment, you will implement a bounded queue class that is thread-safe.

Concurrency and The Producer/Consumer Problem

A producer/consumer problem is one in which two programs, or two independent parts of the same program, are exchanging data.  A queue is often used in producer/consumer problems: the producer adds items to the queue, and consumer removes items from the queue.  The queue allows items to be produced and consumed at any time: for example, the producer can produce an item even if the consumer is not ready to receive it.  The consumer can consume an item in the queue even if the producer is busy.  In other words, the queue allows the producer and consumer to operate asynchronously.

In a producer/consumer problem, the queue that connects the two tasks should be bounded, meaning that it can only hold a fixed number of items.  If the queue is unbounded, then if the producer produces items faster than the consumer can consume them, the queue can quickly grow to a very large number of items (and perhaps exhaust available memory).  Making the queue bounded has the effect that the rates of the producer and consumer are matched.  During times that the queue is full, the producer must wait until some space becomes available.

Concurrency is a term which describes two or more programs (or parts of the same program) operating independently.  Threads are one kind of concurrency: they allow two or more parts of a single program to be executing at the same time.  Two threads can access the same data structure, such as a queue.  However, two allow access from multiple threads to proceed safely, you must synchronize access to any data that will be accessed by multiple threads.

Threads and Synchronization in Java

Java has built-in support for threads and synchronization.  A new thread can be created by creating a new instance of the java.lang.Thread class and passing an object implementing the java.lang.Runnable interface to the Thread's constructor.  When the Thread's start method is called, the run method of the Runnable object is executed at the same time as any other threads that happen to be running.

The synchronized keyword protects regions of code that may be executed by multiple threads.  Let's look at a very simple class that uses this keyword:

public class Counter {
  int count;

  public Counter(int count) {
    this.count = count;
  }

  public void increment() {
    synchronized (this) {
      int origCount = count;
      count = origCount + 1;
    }
  }

  public void decrement() {
    synchronized (this) {
      int origCount = count;
      count = origCount - 1;
    }
  }
}

This class represents a simple integer counter.  Multiple threads may safely use an instance of this class to increment and decrement the count because each access to the variable count (other than in the constructor) is protected by a synchronized block.  When the program runs, only one thread can enter a synchronized block at a time: this ensures that the object's count is changed atomically.

To see why the synchronization is needed, consider what would happen if it were not present, and more than one thread could be executing the increment method at the same time.  Let's assume that there are two threads calling increment on the same Counter object at the same time.  Also assume that the value of the count variable is initially 0.  Here is a possible interleaving of the two threads that causes anomolous behavior:

Thread 1 Thread 2
int origCount = count;  
  int origCount = count;
count = origCount + 1;  
  count = origCount + 1

Both threads read the value of count and assign it to a local variable before either thread assigns a new value to count.  Therefore, the local variable origCount will have the value 0 in both threads.

Both threads then assign the value origCount + 1 to the count instance variable.  So, even though there were two calls to increment, the value of count afterwards is only 1.

Making a Class Thread-Safe

A class is thread-safe if multiple threads can safely access instances of the class without the danger of anomolous behavior.  Here is a simple rule that will allow you to write thread-safe classes:

In a thread-safe class, all methods that may be called from multiple threads must use a synchronized (this) block when accessing the object's fields

By following this rule you will ensure that only one thread at a time is allowed to access shared data.

Wait and Notify

Sometimes, one thread will need to wait until some condition is true before it proceeds.  For example, we could change the Counter class so it supports a waitUntilCounterIsZero method, that causes the thread that calls the method to wait until the value of the counter is 0.

public class Counter {
  int count;

  public Counter(int count) {
    this.count = count;
  }

  public void increment() {
    synchronized (this) {
      int origCount = count;
      count = origCount + 1;

      this.notifyAll();
    }
  }

  public void decrement() {
    synchronized (this) {
      int origCount = count;
      count = origCount - 1;

      this.notifyAll();
    }
  }

  public void waitUntilCounterIsZero() throws InterruptedException {
    synchronized (this) {
      while (count != 0) {
        this.wait();
      }
    }
  }
}

Note that we have added calls to the wait and notifyAll methods.  The wait method suspends the current thread and allows other threads to enter synchronized blocks that are protected by the same object (in this example, this, the instance of the Counter class.)  The notifyAll method "wakes up" any thread that is waiting on the object that one or more conditions associated with the object may have become true.  Here are the rules for using wait and notifyAll:

  1. wait and notifyAll must be called inside a synchronized block, and they must be called on the same object specified in the synchronized block

  2. wait should be called inside a loop that checks the condition being waited for.  The loop should terminate when the condition becomes true.

  3. notifyAll should be called immediately after any code that may make true a condition that another thread might be waiting for.

You may notice that the waitUntilCounterIsZero method is declared to throw InterruptedException.  This is a checked exception that indicates that while a thread was waiting, it was interrupted by some other thread.  Often, when implementing a thread-safe class, the best policy is to simply allow these exceptions to be thrown out of methods that call wait.

Your Task

To get started, import the file assign5.zip into your Eclipse workspace.  (File->Import->Existing Projects into Workspace->Archive File)

Your task is to implement the QueueFactory class.  This class contains a single static method that should create an instance of a class that implements the BoundedQueue<E> interface.  You will need to implement a class that implements this interface.

BoundedQueue<E> defines the following methods:

public void enqueue(E item) throws InterruptedException
Add an item to the tail of the queue.  The method should wait until the queue is not full (containing the maximum number of items) before adding the item.

public E dequeue() throws InterruptedException
Remove an item from the head of the queue.  The method should wait until an item is available before removing the head item.

public SimulationObserver<E> getObserver()
Return the SimulationObserver object that was passed to QueueFactory's createQueue method.  (More about this in a moment.)

public void getContents(List<E> queueContents)
Add each item currently in the queue to the given List object, starting with the head item, and continuing in order to the tail item.

You will need to use synchronization to implement each method.  The enqueue and dequeue methods will need to call the wait and notifyAll methods as appropriate.

Simulation and SimulationObserver

A producer/consumer simulation is provided to test your BoundedQueue implementation.  The simulation creates two threads, a producer and a consumer, that use your queue to communicate data items.  The SimulationObserver interface specifies several methods that inform the simulation's GUI of changes in the status of the producer, consumer, and queue.

In your BoundedQueue implementation, you should call the producerRunning, producerWaiting, consumerRunning, and consumerWaiting methods as appropriate.

The producerWaiting and consumerWaiting methods should be called just before calls to wait in the enqueue and dequeue methods, respectively.  These calls inform the GUI when either producer or consumer is suspended because the queue is full or empty.  The producerRunning and consumerRunning methods should be called after the condition that the producer or consumer is waiting for becomes true, and it can process to either enqueue or dequeue an item.

Testing Your Work

Before testing your queue implementation using the producer/consumer simulation, write a main method that creates and instance of your queue class and enqueues and dequeues some items, verifying that the items are returned in the expected order.  You can run the main method by right-clicking on the source file for your queue class and choosing "Run As->Java Application".  The output will be visible in the Console window.

In your test code, use an instance of the DummyObserver class as the SimulationObserver object your queue will use.

To test your queue implementation with multiple threads, right-click "SimulationFrame.java" and choose "Run As->Java Application".  You will see a window that looks like this:

"Produce time" and "Consume time" are the maximum amount of time that the producer and consumer will need to produce or consume an item, respectively.  As long as you choose a "Produce time" that is less than the "Consume time", the queue will eventually become full and the Producer will have to wait:

Submitting Your Work

From a terminal window:

cd
cd eclipse-workspace
submit102 assign5