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, then 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 e) { System.err.println(e); } catch (NoSuchAlgorithmException e) { System.err.println(e); } } 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 the accessor method 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, then tries to
retrieve the result using getDigest( )
. However,
when you run this program, the result is not what you expect:
D:JAVAJNP2examples 5>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, then dr.getDigest( )
returns null
, and the first attempt to access
digest
throws a
NullPointerException
.
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 may work, and you’ll get the expected output like this:
D:JAVAJNP2examples 5>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, then you’re back where we
started:
D:JAVAJNP2examples 5>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
you’re spawning, 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! You need a better way to guarantee that
the getDigest( )
method isn’t called until
the digest is ready.
The solution most novices adopt is to have 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.
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 back its creator 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 e) { System.err.println(e); } catch (NoSuchAlgorithmException e) { System.err.println(e); } } }
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. That is handled by a separate
method, receiveDigest( )
. This method 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, it 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
needs to know only the name of the
method in CallbackDigestUserInterface
to call.
However, it’s not much harder (and 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 e) { System.err.println(e); } catch (NoSuchAlgorithmException e) { System.err.println(e); } } }
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 a start( )
method. 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 on polling. 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, then 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 the AWT and JavaBeans™. The
AWT runs in a separate
thread from the rest of your 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
addPropertyChange-Listener( )
. 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 void synchronized 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 e) { System.err.println(e); } catch (NoSuchAlgorithmException e) { System.err.println(e); } } }
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( ); } } }
3.137.192.3