Untangling Java concurrency

Java 101: Understanding Java threads, Part 3: Thread scheduling and wait/notify

Learn about the mechanisms that help you set and manage thread priority

1 2 3 4 Page 3
Page 3 of 4

To support the wait/notify mechanism, Object declares the void wait(); method (to force a thread to wait) and the void notify(); method (to notify a waiting thread that it can continue execution). Because every object inherits Object's methods, wait() and notify() are available to all objects. Both methods share a common feature: they are synchronized. A thread must call wait() or notify() from within a synchronized context because of a race condition inherent to the wait/notify mechanism. Here is how that race condition works:

  1. Thread A tests a condition and discovers it must wait.
  2. Thread B sets the condition and calls notify() to inform A to resume execution. Because A is not yet waiting, nothing happens.
  3. Thread A waits, by calling wait().
  4. Because of the prior notify() call, A waits indefinitely.

To solve the race condition, Java requires a thread to enter a synchronized context before it calls either wait() or notify(). Furthermore, the thread that calls wait() (the waiting thread) and the thread that calls notify() (the notification thread) must compete for the same lock. Either thread must call wait() or notify() via the same object on which they enter their synchronized contexts because wait() tightly integrates with the lock. Prior to waiting, a thread executing wait() releases the lock, which allows the notification thread to enter its synchronized context to set the condition and notify the waiting thread. Once notification arrives, the JVM wakens the waiting thread, which then tries to reacquire the lock. Upon successfully reacquiring the lock, the previously waiting thread returns from wait(). Confused? The following code fragment offers clarification:

// Condition variable initialized to false to indicate condition has not occurred.
boolean conditionVar = false;
// Object whose lock threads synchronize on.
Object lockObject = new Object ();
// Thread A waiting for condition to occur...
synchronized (lockObject)
{
   while (!conditionVar)
      try
      {
         lockObject.wait ();
      }
      catch (InterruptedException e) {}
}
// ... some other method
// Thread B notifying waiting thread that condition has now occurred...
synchronized (lockObject)
{
   conditionVar = true;
   lockObject.notify ();
}

The code fragment introduces condition variable conditionVar, which threads A and B use to test and set a condition, and lock variable lockObject, which both threads use for synchronization purposes. The condition variable initializes to false because the condition does not exist when the code starts execution. When A needs to wait for a condition, it enters a synchronized context (provided B is not in its synchronized context). Once inside its context, A executes a while loop statement whose Boolean expression tests conditionVar's value and waits (if the value is false) by calling wait(). Notice that lockObject appears as part of synchronized (lockObject) and lockObject.wait ();—that is no coincidence. From inside wait(), A releases the lock associated with the object on which the call to wait() is made—the object associated with lockObject in the lockObject.wait (); method call. This allows B to enter its synchronized (lockObject) context, set conditionVar to true, and call lockObject.notify (); to notify A that the condition now exists. Upon receiving notification, A attempts to reacquire its lock. That does not occur until B leaves its synchronized context. Once A reacquires its lock, it returns from wait() and retests the condition variable. If this variable's value is true, A leaves its synchronized context.

Caution: If a call is made to wait() or notify() from outside a synchronized context, either call results in an IllegalMonitorStateException.

Apply wait/notify to the producer-consumer relationship

To demonstrate wait/notify's practicality, I introduce you to the producer-consumer relationship, which is common among multithreaded programs where two or more threads must coordinate their activities. The producer-consumer relationship demonstrates coordination between a pair of threads: a producer thread (producer) and a consumer thread (consumer). The producer produces some item that a consumer consumes. For example, a producer reads items from a file and passes those items to a consumer for processing. The producer cannot produce an item if no room is available for storing that item because the consumer has not finished consuming its item(s). Also, a consumer cannot consume an item that does not exist. Those restrictions prevent a producer from producing items that a consumer never receives for consumption, and prevents a consumer from attempting to consume more items than are available. Listing 4 shows the architecture of a producer-/consumer-oriented program:

Listing 4. ProdCons1.java

// ProdCons1.java
class ProdCons1
{
   public static void main (String [] args)
   {
      Shared s = new Shared ();
      new Producer (s).start ();
      new Consumer (s).start ();
   }
}
class Shared
{
   private char c = '\u0000';
   void setSharedChar (char c) { this.c = c; }
   char getSharedChar () { 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++)
      {
           try
           {
              Thread.sleep ((int) (Math.random () * 4000));
           }
           catch (InterruptedException e) {}
           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
      {
         try
         {
            Thread.sleep ((int) (Math.random () * 4000));
         }
         catch (InterruptedException e) {}
         ch = s.getSharedChar ();
         System.out.println (ch + " consumed by consumer.");
      }
      while (ch != 'Z');
   }
}

ProdCons1 creates producer and consumer threads. The producer passes uppercase letters individually to the consumer by calling s.setSharedChar (ch);. Once the producer finishes, that thread terminates. The consumer receives uppercase characters, from within a loop, by calling s.getSharedChar (). The loop's duration depends on that method's return value. When Z returns, the loop ends, and, thus, the producer informs the consumer when to finish. To make the code more representative of real-world programs, each thread sleeps for a random time period (up to four seconds) before either producing or consuming an item.

Because the code contains no race conditions, the synchronized keyword is absent. Everything seems fine: the consumer consumes every character that the producer produces. In reality, some problems exist, which the following partial output from one invocation of this program shows:

consumed by consumer.
A produced by producer.
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.
F produced by producer.
F consumed by consumer.

The first output line, consumed by consumer., shows the consumer trying to consume a nonexisting uppercase letter. The output also shows the producer producing a letter (A) that the consumer does not consume. Those problems do not result from lack of synchronization. Instead, the problems result from lack of coordination between the producer and the consumer. The producer should execute first, produce a single item, and then wait until it receives notification that the consumer has consumed the item. The consumer should wait until the producer produces an item. If both threads coordinate their activities in that manner, the aforementioned problems will disappear. Listing 5 demonstrates that coordination, which the wait/notify mechanism initiates:

Listing 5. ProdCons2.java

// ProdCons2.java

class ProdCons2
{
   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++)
      {
           try
           {
              Thread.sleep ((int) (Math.random () * 4000));
           }
           catch (InterruptedException e) {}

           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
      {
         try
         {
            Thread.sleep ((int) (Math.random () * 4000));
         }
         catch (InterruptedException e) {}

         ch = s.getSharedChar ();
         System.out.println (ch + " consumed by consumer.");
      }
      while (ch != 'Z');
   }
}
</code>

When you run ProdCons2, you should see the following output (abbreviated 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.
G produced by producer.
G consumed by consumer.

The problems disappeared. The producer always executes before the consumer and never produces an item before the consumer has a chance to consume it. To produce this output, ProdCons2 uses the wait/notify mechanism.

The wait/notify mechanism appears in the Shared class. Specifically, wait() and notify() appear in Shared's setSharedChar(char c) and getSharedChar() methods. Shared also introduces a writeable instance field, the condition variable that works with wait() and notify() to coordinate the execution of the producer and consumer. Here is how that coordination works, assuming the consumer executes first:

  1. The consumer executes s.getSharedChar ().
  2. Within that synchronized method, the consumer calls wait() (because writeable contains true). The consumer waits until it receives notification.
  3. At some point, the producer calls s.setSharedChar (ch);.
  4. When the producer enters that synchronized method (possible because the consumer released the lock inside the wait() method just before waiting), the producer discovers writeable's value as true and does not call wait().
  5. The producer saves the character, sets writeable to false (so the producer must wait if the consumer has not consumed the character by the time the producer next invokes setSharedChar(char c)), and calls notify() to waken the consumer (assuming the consumer is waiting).
  6. The producer exits setSharedChar(char c).
  7. The consumer wakens, sets writeable to true (so the consumer must wait if the producer has not produced a character by the time the consumer next invokes getSharedChar()), notifies the producer to awaken that thread (assuming the producer is waiting), and returns the shared character.

Note: To write more reliable programs that use wait/notify, think about what conditions exist in your program. For example, what conditions exist in ProdCons2? Although ProdCons2 contains only one condition variable, there are two conditions. The first condition is the producer waiting for the consumer to consume a character and the consumer notifying the producer when it consumes the character. The second condition represents the consumer waiting for the producer to produce a character and the producer notifying the consumer when it produces the character.

The rest of the family

In addition to wait() and notify(), three other methods make up the wait/notify mechanism's method family: void wait(long millis);, void wait(long millis, int nanos);, and void notifyAll();.

The overloaded wait(long millis) and wait(long millis, int nanos) methods allow you to limit how long a thread must wait. wait(long millis) limits the waiting period to millis milliseconds, and wait(long millis, int nanos) limits the waiting period to a combination of millis milliseconds and nanos nanoseconds. As with the no-argument wait() method, code must call these methods from within a synchronized context.

You use wait(long millis) and wait(long millis, int nanos) in situations where a thread must know when notification arrives. For example, suppose your program contains a thread that connects to a server. That thread might be willing to wait up to 45 seconds to connect. If the connection does not occur in that time, the thread must attempt to contact a backup server. By executing wait (45000);, the thread ensures it will wait no more than 45 seconds.

notifyAll() wakens all waiting threads associated with a given lock—unlike the notify() method, which awakens only a single thread. Although all threads wake up, they must still reacquire the object lock. The JVM selects one of those threads to acquire the lock and allows that thread to run. When that thread releases the lock, the JVM automatically selects another thread to acquire the lock. That continues until all threads have run. Examine Listing 6 for an example of notifyAll():

Listing 6. WaitNotifyAllDemo.java

1 2 3 4 Page 3
Page 3 of 4