5.2 Returning Information from a Thread

     

One of the hardest things for programmers accustomed to traditional, single- threaded procedural models to grasp when moving to a multithreaded environment is how to return information from a thread. Getting information out of a finished thread is one of the most commonly misunderstood aspects of multithreaded programming. The run( ) method and the start( ) method don't return any values. For example, suppose that instead of simply printing out the SHA digest, as in Example 5-1 and Example 5-2, the digest thread needs to return the digest to the main thread of execution. Most people's first reaction is to store the result in a field and provide a getter method, as shown in Example 5-3 and Example 5-4. Example 5-3 is a Thread subclass that calculates a digest for a specified file. Example 5-4 is a simple command-line user interface that receives filenames and spawns threads to calculate digests for them.

Example 5-3. A thread that uses an accessor method to return the result
 import java.io.*; import java.security.*; public class ReturnDigest extends Thread {   private File input;   private byte[] digest;   public ReturnDigest(File input) {    this.input = input;   }   public void run( ) {     try {       FileInputStream in = new FileInputStream(input);       MessageDigest sha = MessageDigest.getInstance("SHA");       DigestInputStream din = new DigestInputStream(in, sha);       int b;       while ((b = din.read( )) != -1) ;       din.close( );       digest = sha.digest( );     }     catch (IOException ex) {       System.err.println(ex);     }     catch (NoSuchAlgorithmException ex) {       System.err.println(ex);     }        }      public byte[] getDigest( ) {     return digest;   } } 

Example 5-4. A main program that uses the accessor method to get the output of the thread
 import java.io.*; public class ReturnDigestUserInterface {      public static void main(String[] args) {        for (int i = 0; i < args.length; i++) {            // Calculate the digest       File f = new File(args[i]);       ReturnDigest dr = new ReturnDigest(f);       dr.start( );              // Now print the result       StringBuffer result = new StringBuffer(f.toString( ));       result.append(": ");       byte[] digest = dr.getDigest( );       for (int j = 0; j < digest.length; j++) {         result.append(digest[j] + " ");       }         System.out.println(result);            }      } } 

The ReturnDigest class stores the result of the calculation in the private field digest, which is accessed via getDigest( ) . The main( ) method in ReturnDigestUserInterface loops through a list of files from the command line. It starts a new ReturnDigest thread for each file and then tries to retrieve the result using getDigest( ) . However, when you run this program, the result may not be what you expect:

 D:\JAVA\JNP3\examples>  java ReturnDigestUserInterface *.java  Exception in thread "main" java.lang.NullPointerException       at ReturnDigestUserInterface.main(ReturnDigestUserInterface.java,        Compiled Code) 

The problem is that the main program gets the digest and uses it before the thread has had a chance to initialize it. Although this flow of control would work in a single-threaded program in which dr.start( ) simply invoked the run() method in the same thread, that's not what happens here. The calculations that dr.start() kicks off may or may not finish before the main( ) method reaches the call to dr.getDigest() . If they haven't finished, dr.getDigest( ) returns null , and the first attempt to access digest throws a NullPointerException .

5.2.1 Race Conditions

One possibility is to move the call to dr.getDigest( ) later in the main() method, like this:

 public static void main(String[] args) {      ReturnDigest[] digests = new ReturnDigest[args.length];      for (int i = 0; i < args.length; i++) {          // Calculate the digest     File f = new File(args[i]);     digests[i] = new ReturnDigest(f);     digests[i].start( );          }        for (int i = 0; i < args.length; i++) {            // Now print the result     StringBuffer result = new StringBuffer(args[i]);     result.append(": ");     byte[] digest = digests[i].getDigest( );     for (int j = 0; j < digest.length; j++) {       result.append(digest[j] + " ");     }       System.out.println(result);          }    } 

If you're lucky, this will work and you'll get the expected output, like this:

 D:\JAVA\JNP3\examples>  java ReturnDigest2 *.java  BadDigestRunnable.java: 73 -77 -74 111 -75 -14 70 13 -27 -28 32 68 -126  43 -27 55 -119 26 -77 6 BadDigestThread.java: 69 101 80 -94 -98 -113 29 -52 -124 -121 -38 -82 39  -4 8 -38 119 96 -37 -99 DigestRunnable.java: 61 116 -102 -120 97 90 53 37 -14 111 -60 -86 -112  124 -54 111 114 -42 -36 -111 DigestThread.java: 69 101 80 -94 -98 -113 29 -52 -124 -121 -38 -82 39  -4 8 -38 119 96 -37 -99 

But let me emphasize that point about being lucky. You may not get this output. In fact, you may still get a NullPointerException . Whether this code works is completely dependent on whether every one of the ReturnDigest threads finishes before its getDigest( ) method is called. If the first for loop is too fast and the second for loop is entered before the threads spawned by the first loop start finishing, we're back where we started:

 D:\JAVA\JNP3\examples>  java ReturnDigest2 ReturnDigest.java  Exception in thread "main" java.lang.NullPointerException         at ReturnDigest2.main(ReturnDigest2.java, Compiled Code) 

Whether you get the correct results or this exception depends on many factors, including how many threads the program spawns, the relative speeds of the CPU and disk on the system where this is run, and the algorithm the Java virtual machine uses to allot time to different threads. This is called a race condition . Getting the correct result depends on the relative speeds of different threads, and you can't control those! We need a better way to guarantee that the getDigest() method isn't called until the digest is ready.

5.2.2 Polling

The solution most novices adopt is to make the getter method return a flag value (or perhaps throw an exception) until the result field is set. Then the main thread periodically polls the getter method to see whether it's returning something other than the flag value. In this example, that would mean repeatedly testing whether the digest is null and using it only if it isn't. For example:

 public static void main(String[] args) {      ReturnDigest[] digests = new ReturnDigest[args.length];      for (int i = 0; i < args.length; i++) {          // Calculate the digest     File f = new File(args[i]);     digests[i] = new ReturnDigest(f);     digests[i].start( );          }        for (int i = 0; i < args.length; i++) {     while (true) {       // Now print the result       byte[] digest = digests[i].getDigest( );       if (digest != null) {         StringBuffer result = new StringBuffer(args[i]);         result.append(": ");         for (int j = 0; j < digest.length; j++) {           result.append(digest[j] + " ");         }           System.out.println(result);         break;       }     }         }        } 

This solution works. It gives the correct answers in the correct order and it works irrespective of how fast the individual threads run relative to each other. However, it's doing a lot more work than it needs to.

5.2.3 Callbacks

In fact, there's a much simpler, more efficient way to handle the problem. The infinite loop that repeatedly polls each ReturnDigest object to see whether it's finished can be eliminated. The trick is that rather than having the main program repeatedly ask each ReturnDigest thread whether it's finished (like a five-year-old repeatedly asking, "Are we there yet?" on a long car trip, and almost as annoying), we let the thread tell the main program when it's finished. It does this by invoking a method in the main class that started it. This is called a callback because the thread calls its creator back when it's done. This way, the main program can go to sleep while waiting for the threads to finish and not steal time from the running threads.

When the thread's run( ) method is nearly done, the last thing it does is invoke a known method in the main program with the result. Rather than the main program asking each thread for the answer, each thread tells the main program the answer. For instance, Example 5-5 shows a CallbackDigest class that is much the same as before. However, at the end of the run( ) method, it passes off the digest to the static CallbackDigestUserInterface.receiveDigest( ) method in the class that originally started the thread.

Example 5-5. CallbackDigest
 import java.io.*; import java.security.*; public class CallbackDigest implements Runnable {   private File input;   public CallbackDigest(File input) {    this.input = input;   }   public void run( ) {     try {       FileInputStream in = new FileInputStream(input);       MessageDigest sha = MessageDigest.getInstance("SHA");       DigestInputStream din = new DigestInputStream(in, sha);       int b;       while ((b = din.read( )) != -1) ;       din.close( );       byte[] digest = sha.digest( );       CallbackDigestUserInterface.receiveDigest(digest,         input.getName( ));     }     catch (IOException ex) {       System.err.println(ex);     }     catch (NoSuchAlgorithmException ex) {       System.err.println(ex);     }        } } 

The CallbackDigestUserInterface class shown in Example 5-6 provides the main( ) method. However, unlike the main( ) methods in the other variations of this program in this chapter, this one only starts the threads for the files named on the command line. It does not attempt to actually read, print out, or in any other way work with the results of the calculation. Those functions are handled by a separate method, receiveDigest( ) . receiveDigest( ) is not invoked by the main( ) method or by any method that can be reached by following the flow of control from the main( ) method. Instead, it is invoked by each thread separately. In effect, receiveDigest( ) runs inside the digesting threads rather than inside the main thread of execution.

Example 5-6. CallbackDigestUserInterface
 import java.io.*; public class CallbackDigestUserInterface {      public static void receiveDigest(byte[] digest, String name) {        StringBuffer result = new StringBuffer(name);     result.append(": ");     for (int j = 0; j < digest.length; j++) {       result.append(digest[j] + " ");     }       System.out.println(result);          }      public static void main(String[] args) {        for (int i = 0; i < args.length; i++) {           // Calculate the digest       File f = new File(args[i]);       CallbackDigest cb = new CallbackDigest(f);       Thread t = new Thread(cb);       t.start( );     }         } } 

Example 5-5 and Example 5-6 use static methods for the callback so that CallbackDigest only needs to know the name of the method in CallbackDigestUserInterface to call. However, it's not much harder (and it's considerably more common) to call back to an instance method. In this case, the class making the callback must have a reference to the object it's calling back. Generally, this reference is provided as an argument to the thread's constructor. When the run() method is nearly done, the last thing it does is invoke the instance method on the callback object to pass along the result. For instance, Example 5-7 shows a CallbackDigest class that is much the same as before. However, it now has one additional field, a CallbackDigestUserInterface object called callback . At the end of the run( ) method, the digest is passed to callback 's receiveDigest( ) method. The CallbackDigestUserInterface object itself is set in the constructor.

Example 5-7. InstanceCallbackDigest
 import java.io.*; import java.security.*; public class InstanceCallbackDigest implements Runnable {   private File input;   private InstanceCallbackDigestUserInterface callback;   public InstanceCallbackDigest(File input,     InstanceCallbackDigestUserInterface callback) {     this.input = input;     this.callback = callback;   }   public void run( ) {     try {       FileInputStream in = new FileInputStream(input);       MessageDigest sha = MessageDigest.getInstance("SHA");       DigestInputStream din = new DigestInputStream(in, sha);       int b;       while ((b = din.read( )) != -1) ;       din.close( );       byte[] digest = sha.digest( );       callback.receiveDigest(digest);     }     catch (IOException ex) {       System.err.println(ex);     }     catch (NoSuchAlgorithmException ex) {       System.err.println(ex);     }        } } 

The CallbackDigestUserInterface class shown in Example 5-8 holds the main( ) method as well as the receiveDigest( ) method used to handle an incoming digest. Example 5-8 just prints out the digest, but a more expansive class could do other things as well, such as storing the digest in a field, using it to start another thread, or performing further calculations on it.

Example 5-8. InstanceCallbackDigestUserInterface
 import java.io.*; public class InstanceCallbackDigestUserInterface {      private File input;   private byte[] digest;      public InstanceCallbackDigestUserInterface(File input) {     this.input = input;   }      public void calculateDigest( ) {     InstanceCallbackDigest cb = new InstanceCallbackDigest(input, this);     Thread t = new Thread(cb);     t.start( );    }      void receiveDigest(byte[] digest) {       this.digest = digest;     System.out.println(this);   }      public String toString( ) {     String result = input.getName( ) + ": ";     if (digest != null) {       for (int i = 0; i < digest.length; i++) {         result += digest[i] + " ";       }       }     else {       result += "digest not available";     }     return result;   }      public static void main(String[] args) {        for (int i = 0; i < args.length; i++) {           // Calculate the digest       File f = new File(args[i]);       InstanceCallbackDigestUserInterface d        = new InstanceCallbackDigestUserInterface(f);       d.calculateDigest( );     }         } } 

Using instance methods instead of static methods for callbacks is a little more complicated but has a number of advantages. First, each instance of the main class ( InstanceCallbackDigestUserInterface , in this example) maps to exactly one file and can keep track of information about that file in a natural way without needing extra data structures. Furthermore, the instance can easily recalculate the digest for a particular file, if necessary. In practice, this scheme proves a lot more flexible. However, there is one caveat. Notice the addition of the calculateDigest( ) method to start the thread. You might logically think that this belongs in a constructor. However, starting threads in a constructor is dangerous, especially threads that will call back to the originating object. There's a race condition here that may allow the new thread to call back before the constructor is finished and the object is fully initialized . It's unlikely in this case, because starting the new thread is the last thing this constructor does. Nonetheless, it's at least theoretically possible. Therefore, it's good form to avoid launching threads from constructors.

The first advantage of the callback scheme over the polling scheme is that it doesn't waste so many CPU cycles. But a much more important advantage is that callbacks are more flexible and can handle more complicated situations involving many more threads, objects, and classes. For instance, if more than one object is interested in the result of the thread's calculation, the thread can keep a list of objects to call back. Particular objects can register their interest by invoking a method in the Thread or Runnable class to add themselves to the list. If instances of more than one class are interested in the result, a new interface can be defined that all these classes implement. The interface would declare the callback methods. If you're experiencing d j   vu right now, that's probably because you have seen this scheme before. This is exactly how events are handled in Swing, the AWT, and JavaBeans. The AWT runs in a separate thread from the rest of the program; components and beans inform you of events by calling back to methods declared in particular interfaces, such as ActionListener and PropertyChangeListener . Your listener objects register their interests in events fired by particular components using methods in the Component class, such as addActionListener( ) and addPropertyChangeListener( ) . Inside the component, the registered listeners are stored in a linked list built out of java.awt.AWTEventMulticaster objects. It's easy to duplicate this pattern in your own classes. Example 5-9 shows one very simple possible interface class called DigestListener that declares the digestCalculated( ) method.

Example 5-9. DigestListener interface
 public interface DigestListener {   public void digestCalculated(byte[] digest); } 

Example 5-10 shows the Runnable class that calculates the digest. Several new methods and fields are added for registering and deregistering listeners. For convenience and simplicity, a java.util.Vector manages the list. The run( ) method no longer directly calls back the object that created it. Instead, it communicates with the private sendDigest( ) method, which sends the digest to all registered listeners. The run( ) method neither knows nor cares who's listening to it. This class no longer knows anything about the user interface class. It has been completely decoupled from the classes that may invoke it. This is one of the strengths of this approach.

Example 5-10. The ListCallbackDigest class
 import java.io.*; import java.security.*; import java.util.*; public class ListCallbackDigest implements Runnable {   private File input;   List listenerList = new Vector( );   public ListCallbackDigest(File input) {    this.input = input;   }     public synchronized void addDigestListener(DigestListener l) {     listenerList.add(l);   }      public synchronized void removeDigestListener(DigestListener l) {     listenerList.remove(l);   }         private synchronized  void sendDigest(byte[] digest) {     ListIterator iterator = listenerList.listIterator( );     while (iterator.hasNext( )) {       DigestListener dl = (DigestListener) iterator.next( );       dl.digestCalculated(digest);     }   }     public void run( ) {     try {       FileInputStream in = new FileInputStream(input);       MessageDigest sha = MessageDigest.getInstance("SHA");       DigestInputStream din = new DigestInputStream(in, sha);       int b;       while ((b = din.read( )) != -1) ;       din.close( );       byte[] digest = sha.digest( );       this.sendDigest(digest);     }     catch (IOException ex) {       System.err.println(ex);     }     catch (NoSuchAlgorithmException ex) {       System.err.println(ex);     }        } } 

Finally, Example 5-11 is a main program that implements the DigestListener interface and exercises the ListCallbackDigest class by calculating digests for all the files named on the command line. However, this is no longer the only possible main program. There are now many more possible ways the digest thread could be used.

Example 5-11. ListCallbackDigestUserInterface interface
 import java.io.*; public class ListCallbackDigestUserInterface implements DigestListener {      private File input;   private byte[] digest;      public ListCallbackDigestUserInterface(File input) {     this.input = input;   }      public void calculateDigest( ) {     ListCallbackDigest cb = new ListCallbackDigest(input);     cb.addDigestListener(this);     Thread t = new Thread(cb);     t.start( );    }      public void digestCalculated(byte[] digest) {       this.digest = digest;     System.out.println(this);   }      public String toString( ) {     String result = input.getName( ) + ": ";     if (digest != null) {       for (int i = 0; i < digest.length; i++) {         result += digest[i] + " ";       }       }     else {       result += "digest not available";     }     return result;   }      public static void main(String[] args) {        for (int i = 0; i < args.length; i++) {           // Calculate the digest       File f = new File(args[i]);       ListCallbackDigestUserInterface d        = new ListCallbackDigestUserInterface(f);       d.calculateDigest( );     }         } } 



Java Network Programming
Java Network Programming, Third Edition
ISBN: 0596007213
EAN: 2147483647
Year: 2003
Pages: 164

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