Threads of Control |
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 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, 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: the first shares a file, the second shares an event queue. 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
TheProducer
generates an integer between 0 and 9 (inclusive), stores it in aCubbyHole
object, and prints the generated number. To make the synchronization problem more interesting, theProducer
sleeps for a random amount of time between 0 and 100 milliseconds before repeating the number generating cycle:Theclass 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) { } } } }Consumer
, being ravenous, consumes all integers from theCubbyHole
(the exact same object into which theProducer
put the integers in the first place) as quickly as they become available.Theclass 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); } } }Producer
andConsumer
in this example share data through a commonCubbyHole
object. And you will note that neither theProducer
nor theConsumer
makes any effort whatsoever to ensure that theConsumer
is getting each value produced once and only once. The synchronization between these two threads actually occurs at a lower level, within theget
andput
methods of theCubbyHole
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 theConsumer
and generates two numbers before theConsumer
has a chance to consume the first one. Thus theConsumer
would skip a number. Part of the output might look like this:Another problem that might arise is when the. . . Consumer #1 got: 3 Producer #1 put: 4 Producer #1 put: 5 Consumer #1 got: 5 . . .Consumer
is quicker than theProducer
and consumes the same value twice. In this situation, theConsumer
would print the same value twice and might produce output that looked like this:Either way, the result is wrong. You want the. . . Producer #1 put: 4 Consumer #1 got: 4 Consumer #1 got: 4 Producer #1 put: 5 . . .Consumer
to get each integer produced by theProducer
exactly once. Problems such as those just described are called race conditions. They arise from multiple, asynchronously executing threads trying to access a single object at the same time and getting the wrong result.To prevent race conditions in our producer/consumer example, the storage of a new integer into the
CubbyHole
by theProducer
must be synchronized with the retrieval of an integer from theCubbyHole
by theConsumer
. TheConsumer
must consume each integer exactly once. The producer/consumer program uses two different mechanisms to synchronize theProducer
thread and theConsumer
thread: monitors, and thenotifyAll
andwait
methods.Monitors
Objects such as theCubbyHole
that are shared between two threads and whose accesses must be synchronized are called condition variables. The Java language allows you to synchronize threads around a condition variable through the use of monitors. Monitors prevent two threads from simultaneously accessing the same variable.The
notifyAll
andwait
MethodsAt a higher level, the producer/consumer example usesObject
'snotifyAll
andwait
methods to coordinate theProducer
andConsumer
's activity. TheCubbyHole
usesnotifyAll
andwait
to ensure that each value placed in theCubbyHole
by theProducer
is retrieved once and only once by theConsumer
.The Main Program
Here's a small stand-alone Java application that creates aCubbyHole
object, aProducer
, aConsumer
, and then starts both theProducer
and theConsumer
.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
Threads of Control