Trail: Essential Java Classes
|
Lesson: Doing Two or More Tasks At Once: Threads
|
|
Synchronizing Threads
The Producer generates an integer
between 0 and 9 (inclusive), stores it in a
CubbyHole object, and prints the
generated number. To make the synchronization problem more
interesting, the Producer sleeps for a random amount of time between 0 and
100 milliseconds before repeating the number generating cycle:
public 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.
public 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 makes 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 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.
Race conditions in the producer/consumer example are prevented by
having the storage of a new integer
into the CubbyHole by the Producer
be synchronized with the retrieval of an integer
from the CubbyHole by
the Consumer.
The Consumer must consume each integer exactly once.
The activities of the Producer and Consumer
must be synchronized in two
ways. First, the two threads must not simultaneously access the
CubbyHole.
A Java thread can prevent this from happening by locking an
object. When an object is locked by one thread and another thread tries
to call a synchronized method on the same object, the second thread
will block until the object is unlocked.
Locking an Object discusses this.
And second, the two threads must do some simple coordination. That is,
the Producer must have some way
to indicate to the Consumer that the
value is ready and the Consumer
must have some way to indicate that the
value has been retrieved.
The Thread class provides a collection of
methods--wait, notify,
and notifyAll--to help threads wait for a
condition and notify other threads of when that condition changes.
Using the notifyAll and wait Methods
has more information.
The Main Program
Here's a small stand-alone Java application
that creates a CubbyHole object, a Producer, a Consumer, and then starts both the Producer
and the Consumer.
public 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
|