The Source for Java Technology Collaboration
User: Password:



   

Java Tech Java Tech: The ABCs of Synchronization, Part 2

by Jeff Friesen
09/15/2004

Contents
Waiting and Notification
   Producer and Consumer
Volatile Variables
A Tour of J2SE 5.0's Synchronizers
   Countdown Latches
   Cyclic Barriers
   Exchangers
   Semaphores
Conclusion
Resources
Answers to Previous Homework

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:

  1. The consumer executes s.getSharedChar() to retrieve a letter.
  2. Inside of that synchronized method, the consumer calls wait() because writeable contains true. The consumer now waits until it receives notification from the producer.

  3. The producer eventually executes s.setSharedChar(ch).

  4. 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().

  5. 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).

  6. The producer exits setSharedChar(char c).

  7. 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.

Pages: 1, 2

Next Page » 

Related Articles

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.

View all java.net Articles.

 Feed java.net RSS Feeds