Previous | Next | Trail Map | Writing Java Programs | Threads of Control


Synchronizing Threads

So far, this lesson has contained examples with independent, asynchronous threads. That is, each thread contained all of the data and methods required for its execution and didn't require any outside resources or methods. In addition, the threads in those examples ran at their own pace without concern over the state or activities of any other concurrently running threads.

However, there are many interesting situations where separate concurrently running threads do share data and must consider the state and activities of those other threads. One such set of programming situations are known as Producer/Consumer scenarios where the Producer generates a stream of data which then is consumed by a Consumer.

For example, you can imagine a Java application where one thread (the producer) writes data to a file while a second thread (the consumer) reads data from the same file. Or, as you type characters on the keyboard, the producer thread places key events in an event queue and the consumer thread reads the events from the same queue. Both of these examples use concurrent threads that share a common resource: a file, an event queue. And because the threads share a common resource, they must be synchronized in some way.

This lesson teaches you about Java thread synchronization through a simple Producer/Consumer example.

Producer/Consumer Example

The Producer generates integers ranging from 0 to 9, stores it in a "CubbyHole" object, prints the generated number, and (just to make the synchronization problem more interesting) the Producer sleeps for a random amount of time between 0 and 100 milliseconds.
class Producer extends Thread {
    private CubbyHole cubbyhole;
    private int number;

    public Producer(CubbyHole c, int number) {
	cubbyhole = c;
	this.number = number;
    }

    public void run() {
	for (int i = 0; i < 10; i++) {
	    cubbyhole.put(i);
	    System.out.println("Producer #" + this.number + " put: " + i);
	    try {
	        sleep((int)(Math.random() * 100));
	    } catch (InterruptedException e) {
	    }
	}
    }
}
The Consumer, being ravenous, consumes all integers from the CubbyHole (the exact same object into which the Producer put the integers in the first place) as quickly as they become available.
class Consumer extends Thread {
    private CubbyHole cubbyhole;
    private int number;

    public Consumer(CubbyHole c, int number) {
	cubbyhole = c;
	this.number = number;
    }

    public void run() {
	int value = 0;
	for (int i = 0; i < 10; i++) {
	    value = cubbyhole.get();
	    System.out.println("Consumer #" + this.number + " got: " + value);
	}
    }
}

The Producer and Consumer in this example share data through a common CubbyHole object. And you will note that neither the Producer nor the Consumer make any effort whatsoever to ensure that the Consumer is getting each value produced once and only once. The synchronization between these two threads actually occurs at a lower level, within the get() and put() methods of the CubbyHole object. However, let's assume for a moment that these two threads make no arrangements for synchronization and talk about the potential problems that might arise in that situation.

One problem arises when the Producer is quicker than the Consumer and generates two numbers before the Consumer has a chance to consume the first one. Thus the Consumer would skip a number. Part of the output might look like this:

    . . .

Consumer #1 got: 3
Producer #1 put: 4
Producer #1 put: 5
Consumer #1 got: 5

    . . .
Another problem that might arise is when the Consumer is quicker than the Producer and consumes the same value twice. In this situation, the Consumer would print the same value twice and might produce output that looked like this:
    . . .

Producer #1 put: 4
Consumer #1 got: 4
Consumer #1 got: 4
Producer #1 put: 5

    . . .
Either way, the result is wrong. You want the Consumer to get each integer produced by the Producer exactly once. Problems, such as those just described, that arise from multiple, asynchronously executing threads trying to access a single object at the same time and getting the wrong result, are called race conditions.

To prevent race conditions in our Producer/Consumer example, the storage of a new integer into the CubbyHole by the Producer must be synchronized with the retrieval of an integer from the CubbyHole by the Consumer. The Consumer must consume each integer exactly once. The Producer/Consumer program uses two different mechanisms to synchronize the Producer thread and the Consumer thread: monitors, and the notify() and wait() methods.

The notify() and wait() Methods

At a higher level, the Producer/Consumer example uses Object's notify() and wait() methods to coordinate their activity. The Producer and Consumer use notify() and wait() to ensure that each value placed in the CubbyHole by the Producer is retrieved once and only once by the Consumer.

The Main Program

Here's a small stand-alone Java application that creates a CubbyHole object, one Producer, one Consumer, and then starts both the Producer and the Consumer.
class ProducerConsumerTest {
    public static void main(String args[]) {
	CubbyHole c = new CubbyHole();
	Producer p1 = new Producer(c, 1);
	Consumer c1 = new Consumer(c, 1);

        p1.start();
        c1.start();
    }
}

The Output

Here's the output of ProducerConsumerTest.
Producer #1 put: 0
Consumer #1 got: 0
Producer #1 put: 1
Consumer #1 got: 1
Producer #1 put: 2
Consumer #1 got: 2
Producer #1 put: 3
Consumer #1 got: 3
Producer #1 put: 4
Consumer #1 got: 4
Producer #1 put: 5
Consumer #1 got: 5
Producer #1 put: 6
Consumer #1 got: 6
Producer #1 put: 7
Consumer #1 got: 7
Producer #1 put: 8
Consumer #1 got: 8
Producer #1 put: 9
Consumer #1 got: 9

Try this: Remove the lines that are shown in bold in the listing of the CubbyHole class shown above. Recompile the program and run it again. What happened? Because no explicit effort has been made to synchronize the Producer and Consumer threads, the Consumer consumes with reckless abandon and gets a whole bunch of zeros instead of getting each integer between 0 and 9 exactly once.


Previous | Next | Trail Map | Writing Java Programs | Threads of Control