Last time,
I began a two-part series that focuses on synchronization in a Java context.
I introduced you to the monitor and lock synchronization concepts, for guarding
critical sections of code from simultaneous execution by multiple threads. I
then revealed Java's synchronized method and synchronized statement implementations
of monitors and locks. Finally, I explained deadlock.
An exploration of Java's synchronization ABCs is not complete without coverage
of thread communication, volatile variables, and synchronizer tools. This
month, you'll learn how Java's waiting and notification mechanism supports thread
communication, and explore a classic example of two threads communicating. You'll
then discover volatile variables and find out how they are related to
synchronization. Finally, you'll tour J2SE 5.0's high-level synchronizer tools.
Note: As with the previous article, this article assumes that you have a basic
understanding of threads and the java.lang.Thread class. It also
assumes you understand what is meant by multiple threads simultaneously
executing code. (Refer to the Note near the beginning of last month's article,
if you aren't sure.)
Waiting and Notification
Synchronized methods and synchronized statements make it possible for threads
to avoid conflicts while sharing a common resource (e.g., two threads sending
data packets over a single network connection). But when coupled with Java's
waiting and notification mechanism, synchronized methods and/or synchronized
statements also allow threads to actively communicate, to cooperate on common
goals.
The waiting and notification mechanism supports communication between threads,
as follows: a thread voluntarily waits until some condition (a
prerequisite for continued execution) occurs. At that time, another thread
notifies the waiting thread, to continue its execution. That communication is
made possible by five methods implemented in the Object class:
public final void notify() wakes up an arbitrary thread that is
waiting to enter the monitor associated with the current object. The thread
cannot proceed until the thread within the monitor exits and releases its
lock. The woken thread competes with any other threads that are competing to
obtain the monitor's lock.
public final void notifyAll() wakes up all threads that are
waiting to enter the monitor associated with the current object. The threads
cannot proceed until the thread within the monitor exits and releases its
lock. The woken threads compete with any other threads that are competing to
obtain the monitor's lock.
public final void wait() throws InterruptedException causes the
invoking thread to wait (and give up its lock) until another thread invokes a
notification method that wakes up the thread (which grabs the lock). This
method throws an InterruptedException object if another thread
previously set the thread's interrupted status.
public final void wait(long timeout) throws InterruptedException
causes the invoking thread to wait (and give up its lock) until another thread
calls a notification method that wakes up the thread (which grabs the lock),
or the invoking thread has waited for timeout milliseconds. This
method throws an InterruptedException object if another thread
previously set the thread's interrupted status.
public final void wait(long timeout, int nanos) throws
InterruptedException causes the invoking thread to wait (and give up
its lock) until another thread calls a notification method that wakes up the
thread (which grabs the lock), or the invoking thread has waited for
timeout milliseconds plus nanos nanoseconds. This
method throws an InterruptedException object if another thread previously set the thread's interrupted status.
Because the synchronization lock is integrated with waiting and notification,
each method above must be called from a synchronized method or a synchronized
statement. Otherwise, an IllegalMonitorStateException object is
thrown from the method.
Note: J2SE 5.0's java.util.concurrent.locks package includes Condition,
an interface that serves as a replacement for Object's wait and notify
methods. You can use Condition implementations to create multiple
condition variables that associate with one Lock object, so that a
thread can wait for multiple conditions to occur.
Producer and Consumer
Now that you know how threads use the waiting and notification mechanism to
communicate, let's examine how that mechanism is used in a practical way. The
classic example of thread communication involves the relationship between two
threads: a producer thread and a consumer thread.
The producer thread produces data items to be consumed by the consumer thread.
For example, the producer obtains data items from a network connection and
passes them to the consumer for processing. Each produced data item is stored
in one shared variable.
Imagine a scenario where the threads do not communicate and run at mismatched
speeds. It would be possible for the producer to produce a new data item and record
it in the shared variable before the consumer has retrieved the previous data
item for processing. It would also be possible for the consumer to retrieve the contents
of the shared variable before a new data item is produced. The result
is a chaotic mess. To overcome those problems, the producer must wait until
it is notified that the previously produced data item has been consumed, and
the consumer must wait until it is notified that a new data item has been produced.
The waiting and notification mechanism supports this communication, and is demonstrated
in the following PC.java application source code. See this article's
code.zip file for that source
file.
public class PC
{
public static void main (String [] args)
{
Shared s = new Shared ();
new Producer (s).start ();
new Consumer (s).start ();
}
}
class Shared
{
private char c = '\u0000';
private boolean writeable = true;
synchronized void setSharedChar (char c)
{
while (!writeable)
try
{
wait ();
}
catch (InterruptedException e) {}
this.c = c;
writeable = false;
notify ();
}
synchronized char getSharedChar ()
{
while (writeable)
try
{
wait ();
}
catch (InterruptedException e) { }
writeable = true;
notify ();
return c;
}
}
class Producer extends Thread
{
private Shared s;
Producer (Shared s)
{
this.s = s;
}
public void run ()
{
for (char ch = 'A'; ch <= 'Z'; ch++)
{
s.setSharedChar (ch);
System.out.println (ch + " produced by "
+ "producer.");
}
}
}
class Consumer extends Thread
{
private Shared s;
Consumer (Shared s)
{
this.s = s;
}
public void run ()
{
char ch;
do
{
ch = s.getSharedChar ();
System.out.println (ch + " consumed by "
+ "consumer.");
}
while (ch != 'Z');
}
}
The application creates a Shared object and two threads that get
a copy of that object's reference. The producer invokes the object's
setSharedChar() method to save each of 26 uppercase letters. The
consumer invokes that object's getSharedChar() method to acquire
each letter.
The waiting and notification mechanism is incorporated into both methods. The
introduction of a Boolean writeable instance field variable into
the same class tracks two conditions (the producer waiting on the consumer to
consume a data item and the consumer waiting on the producer to produce a new
data item) and helps to coordinate the execution of the producer and consumer.
The following scenario, where the consumer executes first, illustrates that
coordination:
The consumer executes s.getSharedChar() to retrieve a letter.
Inside of that synchronized method, the consumer calls wait()
because writeable contains true. The consumer now waits until it
receives notification from the producer.
The producer eventually executes s.setSharedChar(ch).
When the producer enters that synchronized method (which is possible because
the consumer released the lock inside of the wait() method prior to
waiting), the producer discovers writeable's value as true and
does not call wait().
The producer saves the character, sets writeable to false (which
will cause the producer to wait on the next setSharedChar()
invocation if the consumer has not consumed the character by that time), and
invokes notify() to waken the consumer (assuming the consumer is
waiting).
The producer exits setSharedChar(char c).
The consumer wakes up (and re-acquires the lock), sets writeable
to true (which will cause the consumer to wait on the next
getSharedChar() invocation if the producer has not produced a
character by that time), notifies the producer to awaken that thread (assuming
the producer is waiting), and returns the shared character.
When run, the application produces the following output (which I've shortened,
for brevity):
A produced by producer.
A consumed by consumer.
B produced by producer.
B consumed by consumer.
C produced by producer.
C consumed by consumer.
D produced by producer.
D consumed by consumer.
E produced by producer.
E consumed by consumer.
F produced by producer.
F consumed by consumer.
As you can see, the producer always produces a data item before the consumer
consumes that data item. Furthermore, each data item is produced exactly once
and each data item is consumed exactly once.
Java Tech: The ABCs of Synchronization, Part 1
Java's thread support is powerful and comprehensive, but it can also lead to problems if you don't fully understand what you're doing. In his latest Java Tech column, Jeff Friesen introduces the concepts of locks, synchronization, and the dangers of deadlock.