Cooperation between threads
After understanding that threads can collide with each other, and how you keep them from colliding, the next step is to learn how to make threads cooperate with each other. The key to doing this is by handshaking between threads, which is safely implemented using the Object methods wait( ) and notify( ).
Wait and notify
It's important to understand that sleep( ) does not release the lock when it is called. On the other hand, the method wait( ) does release the lock, which means that other synchronized methods in the thread object can be called during a wait( ). When a thread enters a call to wait( ) inside a method, that thread's execution is suspended, and the lock on that object is released.
There are two forms of wait( ). The first takes an argument in milliseconds that has the same meaning as in sleep( ): "Pause for this period of time." The difference is that in wait( ):
The object lock is released during the wait( ).
You can come out of the wait( ) due to a notify( ) or notifyAll( ), or by letting the clock run out.
The second form of wait( ) takes no arguments; this version is more commonly used. This wait( ) continues indefinitely until the thread receives a notify( ) or notifyAll( ).
One fairly unique aspect of wait( ), notify( ), and notifyAll( ) is that these methods are part of the base class Object and not part of Thread, as is sleep( ). Although this seems a bit strange at firstto have something that's exclusively for threading as part of the universal base classit's essential because they manipulate the lock that's also part of every object. As a result, you can put a wait( ) inside any synchronized method, regardless of whether that class extends Thread or implements Runnable. In fact, the only place you can call wait( ), notify( ), or notifyAll( ) is within a synchronized method or block (sleep( ) can be called within nonsynchronized methods since it doesn't manipulate the lock). If you call any of these within a method that's not synchronized, the program will compile, but when you run it, you'll get an IllegalMonitorStateException with the somewhat nonintuitive message "current thread not owner." This message means that the thread calling wait( ), notify( ), or notifyAll( ) must "own" (acquire) the lock for the object before it can call any of these methods.
You can ask another object to perform an operation that manipulates its own lock. To do this, you must first capture that object's lock. For example, if you want to notify( ) an object x, you must do so inside a synchronized block that acquires the lock for x:
synchronized(x) { x.notify(); }
Typically, wait( ) is used when you're waiting for some condition that is under the control of forces outside of the current method to change (typically, this condition will be changed by another thread). You don't want to idly wait while testing the condition inside your thread; this is called a "busy wait" and it's a very bad use of CPU cycles. So wait( ) allows you to put the thread to sleep while waiting for the world to change, and only when a notify( ) or notifyAll( ) occurs does the thread wake up and check for changes. Thus, wait( ) provides a way to synchronize activities between threads.
As an example, consider a restaurant that has one chef and one waitperson. The waitperson must wait for the chef to prepare a meal. When the chef has a meal ready, the chef notifies the waitperson, who then gets the meal and goes back to waiting. This is an excellent example of thread cooperation: The chef represents the producer, and the waitperson represents the consumer. Here is the story modeled in code:
//: c13:Restaurant.java // The producer-consumer approach to thread cooperation. import com.bruceeckel.simpletest.*; class Order { private static int i = 0; private int count = i++; public Order() { if(count == 10) { System.out.println("Out of food, closing"); System.exit(0); } } public String toString() { return "Order " + count; } } class WaitPerson extends Thread { private Restaurant restaurant; public WaitPerson(Restaurant r) { restaurant = r; start(); } public void run() { while(true) { while(restaurant.order == null) synchronized(this) { try { wait(); } catch(InterruptedException e) { throw new RuntimeException(e); } } System.out.println( "Waitperson got " + restaurant.order); restaurant.order = null; } } } class Chef extends Thread { private Restaurant restaurant; private WaitPerson waitPerson; public Chef(Restaurant r, WaitPerson w) { restaurant = r; waitPerson = w; start(); } public void run() { while(true) { if(restaurant.order == null) { restaurant.order = new Order(); System.out.print("Order up! "); synchronized(waitPerson) { waitPerson.notify(); } } try { sleep(100); } catch(InterruptedException e) { throw new RuntimeException(e); } } } } public class Restaurant { private static Test monitor = new Test(); Order order; // Package access public static void main(String[] args) { Restaurant restaurant = new Restaurant(); WaitPerson waitPerson = new WaitPerson(restaurant); Chef chef = new Chef(restaurant, waitPerson); monitor.expect(new String[] { "Order up! Waitperson got Order 0", "Order up! Waitperson got Order 1", "Order up! Waitperson got Order 2", "Order up! Waitperson got Order 3", "Order up! Waitperson got Order 4", "Order up! Waitperson got Order 5", "Order up! Waitperson got Order 6", "Order up! Waitperson got Order 7", "Order up! Waitperson got Order 8", "Order up! Waitperson got Order 9", "Out of food, closing" }, Test.WAIT); } } ///:~
Order is a simple self-counting class, but notice that it also includes a way to terminate the program; on order 10, System.exit( ) is called.
A WaitPerson must know what Restaurant they are working for because they must fetch the order from the restaurant's "order window," restaurant.order. In run( ), the WaitPerson goes into wait( ) mode, stopping that thread until it is woken up with a notify( ) from the Chef. Since this is a very simple program, we know that only one thread will be waiting on the WaitPerson's lock: the WaitPerson thread itself. For this reason it's safe to call notify( ). In more complex situations, multiple threads may be waiting on a particular object lock, so you don't know which thread should be awakened. The solutions is to call notifyAll( ), which wakes up all the threads waiting on that lock. Each thread must then decide whether the notification is relevant.
Notice that the wait( ) is wrapped in a while( ) statement that is testing for the same thing that is being waited for. This seems a bit strange at firstif you're waiting for an order, once you wake up the order must be available, right? The problem is that in a multithreading application, some other thread might swoop in and grab the order while the WaitPerson is waking up. The only safe approach is to always use the following idiom for a wait( ):
while(conditionIsNotMet) wait( );
This guarantees that the condition will be met before you get out of the wait loop, and if you have either been notified of something that doesn't concern the condition (as can happen with notifyAll( )), or the condition changes before you get fully out of the wait loop, you are guaranteed to go back into waiting.
A Chef object must know what restaurant he or she is working for (so the Orders can be placed in restaurant.order) and the WaitPerson who is picking up the meals, so that WaitPerson can be notified when an order is ready. In this simplified example, the Chef is generating the Order objects, then notifying the WaitPerson that an order is ready.
Observe that the call to notify( ) must first capture the lock on waitPerson. The call to wait( ) in WaitPerson.run( ) automatically releases the lock, so this is possible. Because the lock must be owned in order to call notify( ), it's guaranteed that two threads trying to call notify( ) on one object won't step on each other's toes.
The preceding example has only a single spot for one thread to store an object so that another thread can later use that object. However, in a typical producer-consumer implementation, you use a first-in, first-out queue in order to store the objects being produced and consumed. See the exercises at the end of the chapter to learn more about this.
Using Pipes for I/O between threads
It's often useful for threads to communicate with each other by using I/O. Threading libraries may provide support for inter-thread I/O in the form of pipes. These exist in the Java I/O library as the classes PipedWriter (which allows a thread to write into a pipe) and PipedReader (which allows a different thread to read from the same pipe). This can be thought of as a variation of the producer-consumer problem, where the pipe is the canned solution.
Here's a simple example in which two threads use a pipe to communicate:
//: c13:PipedIO.java // Using pipes for inter-thread I/O import java.io.*; import java.util.*; class Sender extends Thread { private Random rand = new Random(); private PipedWriter out = new PipedWriter(); public PipedWriter getPipedWriter() { return out; } public void run() { while(true) { for(char c = 'A'; c <= 'z'; c++) { try { out.write(c); sleep(rand.nextInt(500)); } catch(Exception e) { throw new RuntimeException(e); } } } } } class Receiver extends Thread { private PipedReader in; public Receiver(Sender sender) throws IOException { in = new PipedReader(sender.getPipedWriter()); } public void run() { try { while(true) { // Blocks until characters are there: System.out.println("Read: " + (char)in.read()); } } catch(IOException e) { throw new RuntimeException(e); } } } public class PipedIO { public static void main(String[] args) throws Exception { Sender sender = new Sender(); Receiver receiver = new Receiver(sender); sender.start(); receiver.start(); new Timeout(4000, "Terminated"); } } ///:~
Sender and Receiver represent threads that are performing some tasks and need to communicate with each other. Sender creates a PipedWriter, which is a standalone object, but inside Receiver the creation of PipedReader must be associated with a PipedWriter in the constructor. The Sender puts data into the Writer and sleeps for a random amount of time. However, Receiver has no sleep( ) or wait( ). But when it does a read( ), it automatically blocks when there is no more data. You get the effect of a producer-consumer, but no wait( ) loop is necessary.
Notice that the sender and receiver are started in main( ), after the objects are completely constructed. If you don't start completely constructed objects, the pipe can produce inconsistent behavior on different platforms.
More sophisticated cooperation
Only the most basic cooperation approach (producer-consumer, usually implemented with wait( ) and notify( )/notifyAll( )) has been introduced in this section. This will solve most kinds of thread cooperation problems, but there are numerous more sophisticated approaches that are described in more advanced texts (in particular, Lea, noted at the end of this chapter).