Waiting and Notification
Java provides a small API that supports communication between threads. Using this API, one thread waits for a condition (a prerequisite for continued execution) to exist. In the future, another thread will create the condition and then notify the waiting thread. In this chapter, I introduce you to this API.
The java.lang.Object class provides a Wait-and-Notify API that consists of three wait() methods, one notify() method, and one notifyAll() method. The wait() methods wait for a condition to exist; the notify() and notifyAll() methods notify waiting threads when the condition exists:
The three wait() methods throw java.lang.InterruptedException when any thread interrupted the current thread before or while the current thread was waiting for a notification. The interrupted status of the current thread is cleared when this exception is thrown.
Note A thread releases ownership of the monitor associated with the object whose wait() method is called.
This API leverages an object’s condition queue, which is a data structure that stores threads waiting for a condition to exist. The waiting threads are known as the wait set. Because the condition queue is tightly bound to an object’s lock, all five methods must be called from within a synchronized context (the current thread must be the owner of the object’s monitor); otherwise, java.lang.IllegalMonitorStateException is thrown.
The following code/pseudocode fragment demonstrates the noargument wait() method:
synchronized(obj)
{
while (<condition does not hold>)
obj.wait();
// Perform an action that's appropriate to condition.
}
The wait() method is called from within a synchronized block that synchronizes on the same object as the object on which wait() is called (obj). Because of the possibility of spurious wakeups (a thread wakes up without being notified, interrupted, or timing out), wait() is called from within a while loop that tests for the condition holding and reexecutes wait() when the condition still doesn’t hold. After the while loop exits, the condition exists and an action appropriate to the condition can be performed.
Caution Never call a wait() method outside of a loop. The loop tests the condition before and after the wait() call. Testing the condition before calling wait() ensures liveness. If this test was not present, and if the condition held and notify() had been called prior to wait() being called, it’s unlikely that the waiting thread would ever wake up. Retesting the condition after calling wait() ensures safety. If retesting didn’t occur, and if the condition didn’t hold after the thread had awakened from the wait() call (perhaps another thread called notify() accidentally when the condition didn’t hold), the thread would proceed to destroy the lock’s protected invariants.
The following code fragment demonstrates the notify() method, which notifies the waiting thread in the previous example:
synchronized(obj)
{
// Set the condition.
obj.notify();
}
Notice that notify() is called from a critical section guarded by the same object (obj) as the critical section for the wait() method. Also, notify() is called using the same obj reference. Follow this pattern and you shouldn’t get into trouble.
Note There has been much discussion about which notification method is better: notify() or notifyAll(). For example, check out “Difference between notify() and notifyAll()” (http://stackoverflow.com/questions/14924610/difference-between-notify-and-notifyall). If you’re wondering which method to use, I would use notify() in an application where there are only two threads, and where either thread occasionally waits and needs to be notified by the other thread. Otherwise, I would use notifyAll().
Producers and Consumers
A classic example of thread communication involving conditions is the relationship between a producer thread and a consumer thread. The producer thread produces data items to be consumed by the consumer thread. Each produced data item is stored in a shared variable.
Imagine that the threads are running at different speeds. The producer might produce a new data item and record it in the shared variable before the consumer retrieves the previous data item for processing. Also, the consumer might retrieve the contents of the shared variable before a new data item is produced.
To overcome those problems, the producer thread must wait until it’s notified that the previously produced data item has been consumed, and the consumer thread must wait until it’s notified that a new data item has been produced. Listing 3-1 shows you how to accomplish this task via wait() and notify().
This application creates a Shared object and two threads that get a copy of the object’s reference. The producer calls the object’s setSharedChar() method to save each of 26 uppercase letters; the consumer calls the object’s getSharedChar() method to acquire each letter.
The writeable instance field 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. It helps coordinate execution of the producer and consumer. The following scenario, where the consumer executes first, illustrates this coordination:
Compile Listing 3-1 as follows:
javac PC.java
Run the resulting application as follows:
java PC
You should observe output such as the following excerpt during one run:
W produced by producer.
W consumed by consumer.
X produced by producer.
X consumed by consumer.
Y produced by producer.
Y consumed by consumer.
Z produced by producer.
Z consumed by consumer.
Although the synchronization works correctly, you might observe multiple producing messages before multiple consuming messages:
A produced by producer.
B produced by producer.
A consumed by consumer.
B consumed by consumer.
Also, you might observe a consuming message before a producing message:
V consumed by consumer.
V produced by producer.
Either strange output order doesn’t mean that the producer and consumer threads aren’t synchronized. Instead, it’s the result of the call to setSharedChar() followed by its companion System.out.println() method call not being synchronized, and by the call to getSharedChar() followed by its companion System.out.println() method call not being synchronized. The output order can be corrected by wrapping each of these method call pairs in a synchronized block that synchronizes on the Shared object referenced by s. Listing 3-2 presents this enhancement.
Compile Listing 3-2 (javac PC.java) and run this application (java PC). Its output should always appear in the same alternating order as shown next (only the first few lines are shown 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.
EXERCISES
The following exercises are designed to test your understanding of Chapter 3’s content:
The main() method first creates a runnable for the threads that will wait at the gate. The runnable prints a message stating that the thread is waiting, increments a counter, sleeps for 2 seconds, and waits (make sure to account for spurious wakeups). Upon wakeup, the thread outputs a message stating that the thread is terminating. main() then creates three Thread objects and starts three threads to execute the runnable. Next, main() creates another runnable that repeatedly sleeps for 200 milliseconds until the counter equals 3, at which point it notifies all waiting threads. Finally, main() creates a Thread object for the second runnable and starts the thread.
Summary
Java provides an API that supports communication between threads. This API consists of Object’s three wait() methods, one notify() method, and one notifyAll() method. The wait() methods wait for a condition to exist; notify() and notifyAll() notify waiting threads when the condition exists.
The wait(), notify(), and notifyAll() methods are called from within a synchronized block that synchronizes on the same object as the object on which they are called. Because of spurious wakeups, wait() is called from a while loop that reexecutes wait() while the condition doesn’t hold.
A classic example of thread communication involving conditions is the relationship between a producer thread and a consumer thread. The producer thread produces data items to be consumed by the consumer thread. Each produced data item is stored in a shared variable.
To overcome problems such as consuming a data item that hasn’t been produced, the producer thread must wait until it’s notified that the previously produced data item has been consumed, and the consumer thread must wait until it’s notified that a new data item has been produced.
Chapter 4 presents additional thread capabilities.
18.216.27.251