Recipe 24.7 Synchronizing Threads with wait( ) and notifyAll( )


Problem

The synchronized keyword lets you lock out multiple threads but doesn't give you much communication between them.

Solution

Use wait( ) and notifyAll( ). Very carefully.

Discussion

Three methods appear in java.lang.Object that allow you to use any object as a synchronization target: wait( ), notify( ), and notifyAll( ).


wait( )

Causes the current thread to block in the given object until awakened by a notify( ) or notifyAll( ).


notify( )

Causes a randomly selected thread waiting on this object to be awakened. It must then try to regain the monitor lock. If the "wrong" thread is awakened, your program can deadlock.


notifyAll( )

Causes all threads waiting on the object to be awakened; each will then try to regain the monitor lock. Hopefully one will succeed.

The mechanism is a bit odd: there is no way to awaken only the thread that owns the lock. However, that's how it works, and it's the reason almost all programs use notifyAll( ) instead of notify( ). Also, note that both wait( ) and the notification methods can be used only if you are already synchronized on the object; that is, you must be in a synchronized method within or a code block synchronized on the object that you wish your current thread to wait( ) or notify( ) upon.

For a simple introduction to wait( ) and notify( ), I'll use a simple Producer-Consumer model. This pattern can be used to simulate a variety of real-world situations in which one object is creating or allocating objects (producing them), usually with a random delay, while another is grabbing the objects and doing something with them (consuming them). A single-threaded Producer-Consumer model is shown in Example 24-11. As you can see, no threads are created, so the entire program the read( ) in main as well as produce( ) and consume( ) runs in the same thread. You control the production and consumption by entering a line consisting of letters. Each p causes one unit to be produced, while each c causes one unit to be consumed. So if I run it and type pcpcpcpc, the program alternates between producing and consuming. If I type pppccc, the program will produce three units and then consume them. See Example 24-11.

Example 24-11. ProdCons1.java
public class ProdCons1 {     /** Throughout the code, this is the object we synchronize on so this      * is also the object we wait( ) and notifyAll( ) on.      */     protected LinkedList list = new LinkedList( );     protected void produce( ) {         int len = 0;         synchronized(list) {             Object justProduced = new Object( );             list.addFirst(justProduced);             len = list.size( );             list.notifyAll( );         }         System.out.println("List size now " + len);     }     protected void consume( ) {         Object obj = null;         int len = 0;         synchronized(list) {             while (list.size( ) == 0) {                 try {                     list.wait( );                 } catch (InterruptedException ex) {                     return;                 }             }             obj = list.removeLast( );             len = list.size( );         }         System.out.println("Consuming object " + obj);         System.out.println("List size now " + len);     }     public static void main(String[] args) throws IOException {         ProdCons1 pc = new ProdCons1( );         int i;         while ((i = System.in.read( )) != -1) {             char ch = (char)i;             switch(ch) {                 case 'p':    pc.produce( ); break;                 case 'c':    pc.consume( ); break;             }         }     } }

The part that may seem strange is using list instead of the main class as the synchronization target. Each object has its own wait queue, so it does matter which object you use. In theory, any object can be used as long as your synchronized target and the object in which you run wait( ) and notify( ) are one and the same. Of course, it is good to refer to the object that you are protecting from concurrent updates, so I used list here.

Hopefully, you're now wondering what this has to do with thread synchronization. There is only one thread, but the program seems to work:

> javac +E -d . ProdCons1.java > java ProdCons1 pppccc List size now 1 List size now 2 List size now 3 Consuming object java.lang.Object@d9e6a356 List size now 2 Consuming object java.lang.Object@d9bea356 List size now 1 Consuming object java.lang.Object@d882a356 List size now 0

But this program is not quite right. If I enter even one more c than there are p's, think about what happens. The consume( ) method does a wait( ), but it is no longer possible for the read( ) to proceed. The program, we say, is deadlocked : it is waiting on something that can never happen. Fortunately, this simple case is detected by some versions of the Java runtime:

ppccc List size now 1 List size now 2 Consuming object java.lang.Object@18faf0 List size now 1 Consuming object java.lang.Object@15bc20 List size now 0 Dumping live threads: 'gc' tid 0x1a0010, status SUSPENDED flags DONTSTOP  blocked@0x19c510 (0x1a0010->|) 'finaliser' tid 0x1ab010, status SUSPENDED flags DONTSTOP  blocked@0x10e480 (0x1ab010->|) 'main' tid 0xe4050, status SUSPENDED flags NOSTACKALLOC  blocked@0x13ba20 (0xe4050->|) Deadlock: all threads blocked on internal events Abort (core dumped)

Indeed, the read( ) is never executed because there's no way for produce( ) to get called and so the notifyAll( ) can't happen. To fix this, I want to run the producer and the consumer in separate threads. There are several ways to accomplish this. I'll just make consume( ) and produce( ) into inner classes Consume and Produce that extend Thread, and their run( ) method will do the work of the previous methods. In the process, I'll replace the code that reads from the console with code that causes both threads to loop for a certain number of seconds, and change it to be a bit more of a simulation of a distributed Producer-Consumer mechanism. The result of all this is the second version, ProdCons2, shown in Example 24-12.

Example 24-12. ProdCons2.java
import java.util.*; import java.io.*; public class ProdCons2 {     /** Throughout the code, this is the object we synchronize on so this      * is also the object we wait( ) and notifyAll( ) on.      */     protected LinkedList list = new LinkedList( );     protected int MAX = 10;     protected boolean done = false; // Also protected by lock on list.     /** Inner class representing the Producer side */     class Producer extends Thread {         public void run( ) {             while (true) {                 Object justProduced = getRequestFromNetwork( );                 // Get request from the network - outside the synch section.                 // We're simulating this actually reading from a client, and it                 // might have to wait for hours if the client is having coffee.                 synchronized(list) {                         while (list.size( ) == MAX) // queue "full"                         try {                             System.out.println("Producer WAITING");                             list.wait( );     // Limit the size                         } catch (InterruptedException ex) {                             System.out.println("Producer INTERRUPTED");                         }                     list.addFirst(justProduced);                     if (done)                         break;                     list.notifyAll( );    // must own the lock                     System.out.println("Produced 1; List size now " + list.size( ));                     // yield( );    // Useful for green threads & demo programs.                 }             }         }         Object getRequestFromNetwork( ) {    // Simulation of reading from client             // try {             //     Thread.sleep(10); // simulate time passing during read             // } catch (InterruptedException ex) {             //     System.out.println("Producer Read INTERRUPTED");             // }             return(new Object( ));         }     }     /** Inner class representing the Consumer side */     class Consumer extends Thread {         public void run( ) {             while (true) {                 Object obj = null;                 int len = 0;                 synchronized(list) {                     while (list.size( ) == 0) {                         try {                             System.out.println("CONSUMER WAITING");                             list.wait( );    // must own the lock                         } catch (InterruptedException ex) {                             System.out.println("CONSUMER INTERRUPTED");                         }                     }                     if (done)                         break;                     obj = list.removeLast( );                     list.notifyAll( );                     len = list.size( );                     System.out.println("List size now " + len);                 }                 process(obj);    // Outside synch section (could take time)                 //yield( );              }         }         void process(Object obj) {             // Thread.sleep(nnn) // Simulate time passing             System.out.println("Consuming object " + obj);         }     }     ProdCons2(int nP, int nC) {         for (int i=0; i<nP; i++)             new Producer( ).start( );         for (int i=0; i<nC; i++)             new Consumer( ).start( );     }     public static void main(String[] args)     throws IOException, InterruptedException {         // Start producers and consumers         int numProducers = 2;         int numConsumers = 2;         ProdCons2 pc = new ProdCons2(numProducers, numConsumers);         // Let it run for, say, 30 seconds         Thread.sleep(30*1000);          // End of simulation - shut down gracefully         synchronized(pc.list) {             pc.done = true;             pc.list.notifyAll( ); // Wake up any waiters!         }     } }

I'm happy to report that all is well with this. It runs for long periods of time, neither crashing nor deadlocking. After running for some time, I captured this tiny bit of the log:

Produced 1; List size now 118 Consuming object java.lang.Object@2119d0 List size now 117 Consuming object java.lang.Object@2119e0 List size now 116

By varying the number of producers and consumers started in the constructor method, you can observe different queue sizes that all seem to work correctly.



Java Cookbook
Java Cookbook, Second Edition
ISBN: 0596007019
EAN: 2147483647
Year: 2003
Pages: 409
Authors: Ian F Darwin

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net