Chapter 12 – Managing Multitasking

 

For years, multiprocessing systems have allowed you to run multiple processes (programs) simultaneously. More recently, many operating systems have begun to support multithreading, allowing programmers to spawn multiple subprocesses, or threads, within each process.

Threads differ significantly from processes. In a non-multithreaded system, there are only processes and related processes. Related processes share only open file handles. In a multithreaded system, each process is made up of from 1 to n threads and all the threads in the process generally share global variables as well as open file handles. This sharing of global variables makes it very easy for threads to share work product, while remaining single-mindedly dedicated to their assigned task.

To get a better handle on the shift from single-threaded to multithreaded programming, picture a company where every employee has his own building (complete with telephone system, bathroom, cafeteria, office supplies, and parking lot). Each employee can do his job as long as everything he needs is in his building, a condition the company goes to great expense to try to ensure. Any job that requires access to another employee’s work product is immensely complicated and requires a considerable amount of communication. If the boss wants something done, he has to pick up the phone. This is the single-process/single-thread model. Now, put all those employees into a single building. You immediately get less cumbersome communication and more efficient resource utilization. This is the single-process/multithread model.

Java is an inherently multithreaded language. As such, it depends on operating system support for threads and multitasking. This is one of the reasons that Java has not, as of this writing, been ported to the immensely popular Windows 3.1 operating environment. In fact, to see just how multithreaded Java is, we need only look at Object, the base class for all Java classes. Six of Object’s twelve public methods involve thread control and inter-thread communication.

Creating Threads

Java encapsulates the notion of a thread in a class named, appropriately enough, Thread. Table 6.1 lists the methods for the Thread class. This class incorporates most of the thread attributes and control methods available to programmers who write to the native operating system thread control API.

There are two types of Thread constructors that correspond with the two different ways of getting an object to run in its own thread. The first, most obvious, is to subclass the Thread class. This is entirely reasonable and is the method we use with the AgentDispatcher, which subclasses Thread and creates its own socket. We could have done it the other way around, subclassing Socket and creating a thread, but as the saying goes, “you make your choices and take your chances.” Listing 6.1 demonstrates how AgentDispatcher extends the Thread class so that it will run in its own thread.

Listing 6.1  Extending the Thread Class

/** There is one AgentDispatcher active at any one time. It

maintains a CONSTANT connection from the AgentLauncher to the

Dispatching AgentServer.

@version 1.0

@author John Rodley 12/1/1995

*/

class AgentDispatcher extends Thread {

     Socket s;

     public AgentDispatcher( String ServerName, int port ) {

    setDaemon(true);

           try {

                s = new Socket( InetAddress.getByName(ServerName),port );

                } catch( IOException e )

        {System.out.println("exception "+e ); }

           }

 

/** Dispatch a kill message to all servers telling them to

terminate the named agent with prejudice.

*/

     public void StopAgent(String id) {

           KillMessage km = new KillMessage(id);

           try { s.getOutputStream().write( km.getMessageBytes()); }

           catch( IOException e )

        { System.out.println( "stop output exception "+e); }

           try { s.close(); }

           catch( IOException e1 )

        { System.out.println( "stop close exception "+e1); }

           }

 

/** Tell the dispatching agent server to dispatch this agent.

*/

     public void Dispatch( String Name, String ID, Vector Args ) {

           DispatchMessage dm = new DispatchMessage(Name,ID,Args);

           try { s.getOutputStream().write( dm.getMessageBytes()); }

           catch( IOException e )

       { System.out.println( "dispatch out exception "+e); }

           }

 

/** The main run loop for this thread. Sits in a loop reading

the socket and processing messages. The only messages that

should come in over this socket are StartMessage and

ResultsMessage.

*/

     public void run() {

           byte buffer[] = new byte[8192];

           while( true ) {

                try {

                int ret;

                if(( ret = s.getInputStream().read( buffer )) != 0 )

                     {

                     if( ret < 0 )    {

                           System.out.println( "connection lost");

                           break;

                           }

                     ClientProcess( buffer, ret );

                     }

                } catch( IOException e ) {

                     System.out.println( "IOexception "+e );

                     break;

                     }

                }

           try{s.close();}

    catch(Exception e)

        { System.out.println("run close exception "+e);}

           }

 

/** Parse a message from the dispatching agent server. Only

valid message types are Start and Results. Take appropriate

action for each message type, updating the AgentLauncher

display.

*/

  public void ClientProcess( byte b[], int numbytes ) {

 

  String command;

  int currentOffset = 0;

  String s;

  int messageStart;

  boolean bret = false;

 

  while( currentOffset != numbytes ) {

     messageStart = currentOffset;

       // Every message starts with 4 bytes of command

     command = new String( b, 0, currentOffset, 4 );

       currentOffset += LoadMessage.PREFIX_SIZE;

 

     // Followed by 10 bytes ASCII length of the whole message

       // including command

     String sLength = new String( b, 0, currentOffset, 10 );

       currentOffset += LoadMessage.LOADLEN_SIZE;

    

     if( command.compareTo(ResultMessage.RESULT_PREFIX ) == 0 )

          {

             ResultMessage rm = new ResultMessage();

           rm.parse( b, currentOffset );

          AgentLauncher.currentAgentLauncher.reportResult(

             rm.server, rm.theURL);

             }

     else

          {

             if( command.compareTo(StartMessage.START_PREFIX ) == 0 )

                  {

                StartMessage sm = new StartMessage();

                sm.parse( b, currentOffset );

                AgentLauncher.currentAgentLauncher.addAgentFace(sm.server);

                  }

           else {

                System.out.println( "Message is BOGUS "+b);

        return;

        }

           }

       Integer il = new Integer( sLength );

     currentOffset = messageStart + il.intValue();

       }  // while bytesused

     }

}

 

The theory behind the AgentDispatcher is simple and common to most threaded I/O. The AgentDispatcher needs to handle both sides of the AgentLauncher<->dispatching AgentServer conversation. We want messages from the AgentServer to the AgentLauncher to be handled asynchronously as they occur so that, for instance, a Results message from a particular server will cause the AgentLauncher to change the display for that server as soon as it’s received.

In order to achieve that, we write a run method that is a simple infinite loop that reads the socket and processes whatever it receives by making calls back into the AgentLauncher. We’ll talk more about the details of run methods later, but the key thing to realize here is that the run method is the top-level method of the new operating system thread, and that within the run method, the call to InputStream.read blocks. That is, the call doesn’t return until it has read some data over the socket. Thus, given the sporadic nature of our communications, the new operating system thread spends most of its time sleeping.

There are only two direct references to threads in the AgentDispatcher. The first one is in the declaration itself where we declare that AgentDispatcher extends Thread. Then, within the constructor, we set the thread to be a daemon thread. We’ll discuss daemon threads at more length later in this chapter. Listing 6.2 shows the AgentLauncher creating and starting up a new AgentDispatcher.

Listing 6.2  The AgentLauncher Creating and Starting an AgentDispatcher

/** Dispatch the current agent. currentAgent is an instance of

the agent we wish to dispatch, and he has already configured

himself via Agent.configure. We need to get his arguments as a

Vector of Strings, create a dispatcher, then tell the

dispatcher to dispatch using the currentAgent with those

arguments.

@see Agent

@see AgentDispatcher

*/

public void Dispatch() {

     Vector v = currentAgent.getArguments();

     currentDispatcher =

      new AgentDispatcher( disServerName, disServerPort );

     currentDispatcher.start();

     currentDispatcher.Dispatch( currentClassName, currentID, v );

     changeAppState( AGENT_DISPATCHED );

     }

 

Remember that AgentLauncher is our top-level object—our applet—and AgentLauncher.Dispatch is invoked when the user clicks on the Dispatch button. Within Dispatch we instantiate AgentDispatcher, which gives us, via subclassing, a Thread object. Then we call Thread.start to get the actual operating system thread running. The invocation of Thread.start is the key call in the whole thread creation process. Without it, there will not be a new operating system thread.

Though there are only those two direct references to threads in the AgentDispatcher, it is nonetheless a good example of a threaded object because it encapsulates some functionality that executes within the new operating system thread, and some that does not. In this example, AgentLauncher.Dispatch (the AgentDispatcher constructor), AgentDispatcher.Dispatch, and AgentDispatcher.StopAgent all run in one thread, while AgentDispatcher.run and AgentDispatcher.ClientProcess run in another. Later on, when we discuss the start and run methods, we’ll find out how to tell which methods execute in which threads.

Thread Groups

You may have noticed that some of the Thread constructors take a ThreadGroup argument. In Chapter 5 we saw how Java groups Components into Containers to facilitate operations on collections of Component objects. ThreadGroup performs a similar function in thread control—to group threads into one lump where they can be operated on as a group, and to protect threads from each other.

The Runnable Interface

In a multiple-inheritance situation, the subclassing constructor would be all you’d need because all runnable objects could simply inherit Thread with all their other superclasses. But Java doesn’t allow multiple inheritance, and it doesn’t seem reasonable to create a long inheritance branch (with Thread up near the root) simply to make something runnable.

Fortunately, there’s a second style of Thread constructor. This one takes an object that implements the Runnable interface as one of its parameters. We use this constructor with our AgentLauncher applet which, in its start method, creates a Thread and passes itself as the parameter to the Thread constructor. This constructor was specifically designed to deal with situations where you don’t want to subclass Thread.

The Runnable interface has only a single method: run. The run method contains the main loop of a Runnable object. Our AgentDispatcher’s run method, for example, contains a loop that simply reads a socket.

Thus we have two classes, AgentLauncher and AgentConnectionHandler, that run in their own thread but get there by different routes. AgentLauncher is a subclass of Thread, while AgentConnectionHandler is a subclass of Object, which implements the Runnable interface. Listing 6.3 shows the parts of the AgentLauncher that relate to its use of threads and the Runnable interface.

Listing 6.3  The AgentLauncher Implementing the Runnable Interface

public final class AgentLauncher extends Applet implements Runnable {

 

/** The main thread */

Thread myThread = null;

 

/** start - Whenever a user visits our page, Applet calls

this function. This is an override of an Applet method. It creates a

thread and passes this AgentLauncher into it. It works because

we implement the Runnable method. */

  public void start() {

     currentAgentLauncher = this;

     if( myThread == null ) 

          {

           myThread = new Thread( this );

           myThread.setName( "AgentLauncher" );

          myThread.start();

           }

    }

 

In Chapter 4, we discussed that Applet.start is called whenever the user “visits” the page containing our applet. When we come into start for the first time, we want to get our AgentLauncher’s run method executing in a new operating system thread. In order to do that, we use a new style of Thread constructor, one that takes a Runnable object as one of its arguments. In Listing 6.3, the AgentLauncher itself (in the start method) is the Runnable object. When, in AgentLauncher.start, we call myThread.start, we get the same effect as we got in Listing 6.1—the run method of our AgentLauncher executes in a new operating system thread.

The start and run Methods

When I think of a Thread object, what immediately comes to mind is a thread that begins execution with the constructor of the object, and terminates after the destructor returns. This is most emphatically not the case with Java Thread objects (or almost any objectified threads). The Thread constructor and the start method both execute in the thread from which they were invoked. Only the run method executes in the new thread. Of course, this means that any method invoked from within the run method will also execute in the new thread.

No matter which type of Thread you create, it’s important to realize just what methods execute in which thread. Listing 6.4 shows the creation of an AgentDispatcher and shows which thread each line of code executes in.

Note: The 1, 2, and question mark that begin some lines in the code in Listing 6.4 are only there for your edification; they would not appear in the actual Java code.

Listing 6.4  Using Operating System Threads

public void Dispatch() {

1   Vector v = currentAgent.getArguments();

1   currentDispatcher =

      new AgentDispatcher( disServerName, disServerPort );

1   currentDispatcher.start();

1   currentDispatcher.Dispatch( currentClassName, currentID, v );

1   changeAppState( AGENT_DISPATCHED );

     }

 

class AgentDispatcher extends Thread {

     Socket s;

1   public AgentDispatcher( String ServerName, int port ) {

1    setDaemon(true);

1         try {

1              s = new Socket( InetAddress.getByName(ServerName),port );

1              } catch( IOException e )

1        {System.out.println("exception "+e ); }

1         }

 

     public void StopAgent(String id) {

?         KillMessage km = new KillMessage(id);

?         try { s.getOutputStream().write( km.getMessageBytes()); }

?         catch( IOException e )

?        { System.out.println( "stop output exception "+e); }

?         try { s.close(); }

?         catch( IOException e1 )

?        { System.out.println( "stop close exception "+e1); }

           }

 

     public void Dispatch( String Name, String ID, Vector Args ) {

?         DispatchMessage dm = new DispatchMessage(Name,ID,Args);

?         try { s.getOutputStream().write( dm.getMessageBytes()); }

?         catch( IOException e )

?       { System.out.println( "dispatch out exception "+e); }

           }

 

     public void run() {

2         byte buffer[] = new byte[8192];

2         while( true ) {

2              try {

2              int ret;

2              if(( ret = s.getInputStream().read( buffer )) != 0 )

                     {

2                   if( ret < 0 )    {

2                         System.out.println( "connection lost");

2                         break;

                           }

2                   ClientProcess( buffer, ret );

                     }

2              } catch( IOException e ) {

2                   System.out.println( "IOexception "+e );

2                   break;

                     }

                }

2         try{s.close();}

2    catch(Exception e)

2        { System.out.println("run close exception "+e);}

           }

 

public void ClientProcess( byte b[], int numbytes ) {

2   All code excecutes in 2

     }

}

 

In this example, AgentLauncher.Dispatch starts off running in thread 1. It then creates an AgentDispatcher, and starts it running in its own thread via Thread.start. Thread.start creates the new thread, thread 2, and starts it running our run method. All the code in run executes in thread 2. In addition, every method invoked from run also executes in thread 2, so ClientProcess all happens within thread 2. Notice that, in this example, we can’t specify which thread StopAgent and Dispatch run in. If the user presses the Kill or Dispatch buttons, then these methods are invoked from thread 1, and thus run in thread 1. They might, however, be invoked in response to a message from the AgentServer. In that case, the message comes over the socket and is read in the run method that executes in thread 2. The message is passed to ClientProcess, still in thread 2, and anything invoked by ClientProcess runs in thread 2.

Though we call the whole class Thread, you should really think of the run method and the underlying operating system thread as one and the same. The first line of the run method is the first line of Java code executed by the new thread, and the return from the run method is the last line of Java code executed by the new thread. This is why every run method you see will be some sort of loop. In essence, the run method is the equivalent of main() in a single-threaded C program. Everything that happens in the new thread starts within the run method. (Conversely, if you can’t trace an instruction back to somewhere within the run method, it didn’t happen in that thread!) When the run method returns, the thread disappears from the system. In fact, run never really returns. Because it executes independently, it has nowhere to return to.

The run method is never explicitly called. Instead, we invoke the start method which causes Java to create a new operating system thread that executes the run method. One of the most frustrating mistakes you can make in Java is to create a Thread, then forget to invoke Thread.start(). Another mistake (and yes, I’ve made it) is to invoke run rather than start. In that case, run executes in the invoking thread, and the code you thought would execute in the invoking thread never does, because run is running and doesn’t return immediately the way start would.

Another thing to keep in mind is that Applet.start and Thread.start are fundamentally different things. Applet.start is designed to be overridden to do whatever needs doing when the user leaves a page. Thread.start should never be overridden, because it contains the functionality that actually creates the new operating system thread.

Thread Control

We’ve seen that the Thread object and the underlying operating system thread are not the same thing. You can actually have a Thread object without an underlying operating system thread. There are two points in a Thread’s life when this is the case: before the start method is invoked (the thread hasn’t been created yet) and after the run method returns (the thread has already disappeared.).

Thus, the Thread class is actually made up of the underlying operating system thread (the run method) and a set of thread monitoring and control methods. Some methods can be invoked from outside the thread (outside the run method) and some can’t. Table 6.2 shows which Thread methods can be invoked on the current thread, and which need to be invoked against an external thread.

start and stop are used to create and destroy the underlying operating system thread. You must invoke start to create the thread, but you don’t need to use stop to get rid of it. Most applets rely on the return from the run method to destroy the thread.

We can also pause a thread with the suspend method. This method halts thread execution but leaves the thread in memory, allowing us to resume execution right where we left off when suspend was invoked. Later on, with animation, we create a series of threads, and suspend or resume them according to the activity of the remote agents.

join is another important thread-control mechanism that we’ll use later on in our applet initialization methods. The point of calling join is to simply wait for the thread you’re calling join against to die. For Unix types, it’s the equivalent of waiting for a child process. join is actually an easy concept, made difficult by a stupid label. In real life, if I join you, we both continue to exist. In threads, when one thread joins another, the one joined must die before the joiner can continue. By all rights, this method should have been named “waitForTheDeathOf.”

Thread Attributes

Each thread has three attributes that can be set and queried: name, priority, and daemon status. The thread name is just that, an identifier we can attach to the Thread. We set the thread names within the Agent system, but never use them. I did this mostly for debugging/system-monitoring purposes. Java itself doesn’t use thread names for anything and there’s no requirement for thread names to be unique. Internally to Java, threads are identified by the unique handle the operating system gives them.

Thread priority is also a straightforward concept, exactly analogous to process priority under Unix. Higher priority threads are scheduled (by the operating system) to run more frequently than lower priority threads. Priority can be changed on the fly via Thread.setPriority. Different operating systems use different ranges for priority values. Some even flip the precedence, with lower values getting higher priority. Thus, Java defines three constants, MIN_PRIORITY, NORM_PRIORITY, and MAX_PRIORITY. Any time you set the priority of a Java Thread, use a number based on these constants.

The concept of daemon status, though common to most multithreaded systems, is a little more difficult to explain. The Java interpreter also will not exit until all non-daemon threads have terminated. In essence, the interpreter joins all non-daemon threads before exiting. Giving a thread daemon status essentially frees it from that control, allowing the interpreter to exit without waiting for the thread to terminate. A thread should be made daemon unless it absolutely must do some cleanup (other than memory management), like closing open files, before the system exits.

Daemon status is set via Thread.setDaemon, which must be invoked before the Thread’s start method gets invoked (that is, before the underlying system thread actually starts executing). I generally put it in the constructor.

When Do Applet Threads Run?

One of the first questions that arises when you start spawning multiple threads is determining when each of these threads will be running. Consider the following scenario: a user enters a page that contains an applet and a single link to another HTML page. The browser reads the <applet> tag, loads the applet, and sets it running. The user then clicks on the hyperlink, taking him to another page. The page with your applet in it disappears, but what happens to the thread running on the user’s machine that contains your applet code? The short answer, as you might expect, is that it stops running. No surprise there.

Especially for our application though, this presents real problems. For one thing, we want to view partial results—that is, we want to follow a hyperlink to a results page while our agents are still in the field collecting more. The problem there is that if the AgentLauncher stops running, the agents in the field will stop dispatching and eventually die off.

What saves us in this situation is that any threads created by our AgentLauncher can be left running while the user hyperlinks elsewhere. All we have to do is break one of the cardinal rules of applet writing, which says that when the browser invokes Applet.stop against your applet, you should stop running. When the browser invokes Applet.stop against our AgentLauncher, the only thread we terminate is the one that updates the display window, which of course we don’t need when the user is looking at another page. All the other threads remain running.

Shared Resources and Synchronization

One issue that single-threaded programmers never have to deal with is coordinating access to shared resources. By shared resources, we mean anything that more than one thread needs to use. In most cases, this means a file or a block of memory. We need to coordinate access because we’re doing a series of operations on the resource that need to happen in one lump so that other threads don’t see partial changes.

One of the ways we coordinate access to shared resources is via synchronized methods/blocks. There are many places in our application where synchronized execution might be useful, but the easiest to understand is probably the reportResults method in the AgentServer. When an agent finishes his work on an AgentServer, he connects to the dispatching AgentServer and sends message containing a single URL pointing to his results HTML document. The dispatching AgentServer combines all these URLs into a single results page the user can jump to via the Results button.

Each SocketHandler on the AgentServer runs in a separate thread. When the SocketHandler reads a Results message over that connection, it calls AgentServer.ReportResults to add that URL to the list of results URLs. You might think that funneling result reporting through a single function guarantees thread safety but that’s not the case because all the calls to reportResults are happening in different threads.

Consider what happens when two agents, 007 and 99, report a result at the same time. 007 reports his results in http://www.info.com/result.html, while agent 99 reports her results in http://www.kaos.com/max.html. What we’re hoping to see in the dispatching AgentServer results HTML document are two lines like those shown in the following code snippet:

 

Results from server <A HREF="http://www.info.com/result.html">www.info.com</A>.

     Time elapsed 3:21 qty 13k - price $2.30

Results from server <A REF="http://www.kaos.com/max.html">www.kaos.com</A>

     Time elapsed 1:15 qty 39k - price FREE

 

What would happen, though, if the two calls to AgentServer.reportResults run at exactly the same time? You could end up with an HTML page like that shown here:

 

Results from server <A HREF="http://www.info.com/result.html">www.info.com</A>

Results from server <A HREF="http://www.kaos.com/max.html">www.kaos.com</A>

     Time elapsed 1:15 qty 39k - price FREE

     Time elapsed 2:30 qty 13k - price $2.30

 

That, of course, would be incorrect. One way we could remedy this situation would be to synchronize access to AgentServer.reportResults, as shown in Listing 6.5 with a synchronized reportResults method.

Listing 6.5  A Synchronized Method

/** Add this Agents results to the cumulative results page that

we've been amassing for the whole run of this agent.

@param  r The ResultMessage that the agent is sending back to

the AgentLauncher.

*/

public synchronized void reportResults( ResultMessage r ) {

  // If this search has already been cancelled, don't bother

  if( resultFile == null )

    return;

  if( r.theURL == null ) {

    try {

      FileOutputStream fos =

        new FileOutputStream( resultFile );

      PrintStream ps = new PrintStream( fos );

      ps.println( "<P>" );

      ps.println( "Results from server <A HREF=\""+r.theURL+

          "\">"+r.server+"</A>");

      ps.println( "\tprice "+r.price);

      if( r.comment != null )

        ps.println( "\t"+r.comment );

      }

    catch( IOException ioe ) {

      System.out.println( "reportResults ioexc "+ioe );

      }

    }

  }

 

The purpose of this method is to print a String to the dispatching AgentServer’s results file. The content of the String depends on the content of the ResultMessage that is supplied as an argument. The ResultMessage simply tells us where the agent stored his results and how much they’ll cost to view. See Chapter 7 for the details of ResultMessage. If the URL is null, then there were no results and we display that (via showNoResults).

The key feature of this method is the synchronized keyword in the declaration. The situation we want to avoid is one where two threads invoke the method at the same time and our calls to println get interlaced as we talked about earlier. The synchronized keyword in the declaration guarantees that only one copy of the method can be running at one time. Another way to put this is that our synchronized AgentLauncher.reportResults can now only run in one thread at a time.

Efficient Serialization

Our synchronized reportResults method does what we need, but it actually does more locking than we really need. Synchronizing the method locks the entire method when all we really need to do is lock the block of code containing the two invocations of println. So we rewrite reportResults to use a synchronized block around those two lines, as shown in Listing 6.6.

Listing 6.6  Using a Synchronized Block

/** Add this agents results to the cumulative results page that

we've been amassing for the whole run of this agent.

@param  r The ResultMessage that the agent is sending back to

the AgentLauncher.

*/

public void reportResults( ResultMessage r ) {

  // If this search has already been cancelled, don't bother

  if( resultFile == null )

    return;

  if( r.theURL == null ) {

    try {

      FileOutputStream fos =

        new FileOutputStream( resultFile );

      PrintStream ps = new PrintStream( fos );

            synchronized( ps ) {

           ps.println( "<P>" );

        ps.println( "Results from server <A HREF=\""+r.theURL+

                "\">"+r.server+"</A>");

              ps.println( "\tprice "+r.price);

              if( r.comment != null )

                ps.println( "\t"+r.comment );

              }

      }

    catch( IOException ioe ) {

      System.out.println( "reportResults ioexc "+ioe );

      }

    }

  }

 

The method itself is no longer synchronized. Multiple threads can invoke the method without blocking waiting for another thread to finish with it. What we’ve added is a synchronized block within the method, which creates the smallest possible window of time where one thread might be waiting for another to finish working.

Each object in the system has a lock associated with it. In most cases, the lock is not used. Various threads can use the object without restriction. However, when thread A invokes a synchronized method (or block), it “acquires” the lock for the object associated with that synchronized method (or block). If thread B has already acquired the lock, thread A waits until thread B releases the lock before it executes the synchronized method. In our final reportResults method, we use the lock on our PrintStream, ps,  to synchronize our block. This locking mechanism is exactly analogous to the more familiar record, table, and file locking that current operating systems and DBMSs provide.

As in DBMS record and table locking, we try never to lock more code than is absolutely necessary. Each time a thread fails to acquire a lock, it (and possibly the user) waits until the lock is released. This is wasted time that should be kept to an absolute minimum.

Our first, unsynchronized reportResults had the potential to misorder lines, but not to intermix the characters of one line with another. This is because println itself is synchronized. The moral: considerable care has been taken with the multithreading issues in Java, so it’s often the case that the level of synchronization you need is already implemented. Don’t go around synchronizing blocks and methods until you’re sure the synchronization you need isn’t already there.

It’s often hard for people to grasp the pervasiveness of synchronization situations, especially if they’re new to the multithreading world. For a quick indication, flip through the class documentation and see just how many of the methods are synchronized.

Subterranean Threads

You now have the wherewithal to go out and create billions and billions of threads of your very own, but even if your applet never explicitly creates a single thread of its own, there are already multiple threads in any Java applet. These are the four subterranean threads that always exist in a running Java program: the main interpreter thread, the finalizer thread, the idle thread, and the garbage collector. The interpreter thread is the easiest to understand because its sole purpose is to execute the compiled Java byte codes from the .class file. You can think of a traditional, single-threaded interpreter as one in which all the functionality is stuffed into the interpreter thread.

The idle thread, the garbage collector, and the finalizer work together. The idle thread, running at a very low priority, simply maintains a flag that tells whether or not it has run. The idle thread’s priority is chosen so that it almost never runs unless all other threads in the Java runtime are blocked. The garbage collector, running at an even lower priority than the idle thread, checks whether the idle thread has run. If the idle thread has been running, that probably means that it’s a good time to run a garbage collection. The finalizer thread is a low-priority thread that helps with garbage collection. After the garbage collector identifies an object as inaccessible, the finalizer calls the object’s finalize method, if it exists, to do any cleanup that might be necessary. The system tries to run garbage collection and finalization only when there is CPU time to spare. This design provides good performance for event-driven applications where the majority of realtime is spent waiting for events.

Other Threads

The subterrannean threads exist in any Java program—applet or application. Applets have another whole class of peer threads—the browser threads. Web browsers are all multithreaded by design. When you see a browser changing the status line, updating the screen, and responding to user input all while downloading a graphics file, that’s a result of conscious multithreaded design. In most browsers, applets will not have access to any threads other than those the applet itself creates, but you should be aware that, in any browser, threads are coming and going all the time.

Inter-Process Communication

Java does not really support the notion of inter-process communication (IPC), not much of a surprise since it doesn’t really believe in processes either. This has important implications for coders who’ve written multithreaded or multiprocess applications in C. As language features, DDE, semaphores, mailboxes, queues, OLE, system-wide shared memory, and most other traditional forms of IPC are absent.

There is only one form of IPC that is provided for Java programs—anonymous pipes via the Process and Runtime classes. In brief, Runtime.exec gets you a Process object, from which you can get Stream objects connected to the process standard in, standard out, and standard error. For security reasons, Runtime.exec is usually unavailable to applets. Thus, it’s use is beyond the scope of this book.

Inter-Thread Communication

Java provides anonymous pipes for inter-thread communication via the PipedInputStream and PipedOutputStream classes. These are classic anonymous pipes that function just like the ones we’re all familiar with from Unix. The basic theory of PipedStreams is that you create one end, then create the other end, passing it the handle to the first end in the constructor.

Inter-Applet Communication

Using the AppletContext interface we talked about in Chapter 4, it is relatively easy to get applets to communicate. In fact, once an applet retrieves an Applet object from the AppletContext, communication is as easy as invoking a method or setting a public variable. There is no need to use extraordinary mechanisms like sockets or anonymous pipes. Listing 4.8 shows an example of a relatively unfriendly applet-to-applet conversation.

When to Thread

We’ve seen how useful and necessary threads are, so by now you should be bursting to turn every Integer into its own thread. Hold on and take a deep breath. Threads are useful and necessary, but they have limitations that you need to consider too.

For one thing, context switching involves some amount of overhead, so every thread you create adds some drag to the overall system—the operating system spends cycles switching among threads, and the more threads, the more cycles the OS spends context switching.

Threads are also a system resource and most operating systems have a hard limit on both the total number of threads in the system and the number of threads that one user can create. Think of it this way: if you spend 500 threads making “active” integers for your rotisserie baseball league scoring application, then your DBMS may run out of transaction processing threads—a bad tradeoff. For total threads, a number somewhere around one thousand is typical. Per-user threads will be something less than that.

When deciding whether or not to thread an object, I tend to concentrate first on the difference between active and passive objects. A network socket is an active object, an integer is not, keyboard is active, file is not. Most objects that respond to events generated outside the application deserve their own threads.

A final reason to get with the program when it comes to threads: Java provides weak to non-existent support for polling I/O. There is no analogy to the Unix select system call which so many Unix programmers rely on for polling I/O. If you intend to do network I/O you will probably want to write it as we have here, as blocking I/O in a separate thread.

Animation

So far, we’ve threaded our Applet and AgentDispatcher communication classes. That’s all well and good, but efficient multitasking and well-structured
communication code is not what brought us here, is it? Admit it, you really bought this book just to put cartoons on your home page, didn’t you?

Well having gone through the applet, awt, and lang packages, we now have all the tools to do that. What we’ll do now, is modify the AgentFace class so that while the agent is off doing his work on the remote AgentServer, the corresponding graphic representation on the user’s screen will be animated. From a UI standpoint, this is a major advance, as the activity on the screen better represents the activity happening behind the scenes out on the Net.

Wait and Notify

We implement our animation the same way the old cartoonists used to—by flashing a new image on the screen at regular intervals to give the illusion of motion. To do that, we need to load a potentially large series of images into memory over the network. This image loading is not just a lengthy operation, but it is also unpredictably lengthy. For this reason, browsers retrieve images in a separate thread. We cannot tell from a call to Applet.getImage whether or not the image we’re trying to draw has been loaded over the Net yet. In fact, if we simply call getImage for all our images, then try to draw them with Component.drawImage many of them will not actually get drawn for a long time. The only indication we ever get that our images have not been loaded is a bad return value from drawImage.

How do we deal with this situation? We could simply sit in a loop calling drawImage until the bad return value changes, but that wastes a lot of CPU cycles. There’s a better way. Using the ImageObserver interface and the wait-notify thread-control mechanism we can have our main Applet thread block (sleep) until all the images are loaded. Listing 6.7 shows how the AgentLauncher implements an ImageObserver/wait-notify solution to image loading.

Listing 6.7  Image Loading in the AgentLauncher

public class AgentLauncher extends Applet implements Runnable {

 

/** The set of gifs to cycle through after an agent reports no results. */

String notstartedgifs[] = {

     "notstarted7.gif",

     "notstarted6.gif",

     "notstarted5.gif",

     "notstarted4.gif",

     "notstarted3.gif",

     "notstarted2.gif",

     "notstarted1.gif"

};

 

/** The set of gifs to cycle through after an agent reports no results. */

String runninggifs[] = {

     "running7.gif",

     "running6.gif",

     "running5.gif",

     "running4.gif",

     "running3.gif",

     "running2.gif",

     "running1.gif"

};

 

/** The set of gifs to cycle through after an agent reports no results. */

String noresultgifs[] = {

     "noresult7.gif",

     "noresult6.gif",

     "noresult5.gif",

     "noresult4.gif",

     "noresult3.gif",

     "noresult2.gif",

     "noresult1.gif"

};

 

/** The set of gifs to cycle through after an agent reports no results. */

String resultgifs[] = {

     "result7.gif",

     "result6.gif",

     "result2.gif",

     "result3.gif",

     "result4.gif",

     "result5.gif",

     "result4.gif",

     "result3.gif",

     "result2.gif",

     "result1.gif"

};

 

/** The set of gifs to cycle through after an agent reports no results. */

String dispatchedgifs[] = {

     "dispatched7.gif",

     "dispatched6.gif",

     "dispatched5.gif",

     "dispatched4.gif",

     "dispatched3.gif",

     "dispatched2.gif",

     "dispatched1.gif"

};

 

/** Override of Applet.init

*/

  public void init() {

    ..

     LoadAnimationImages();

  }

 

/** Load all the images this applet needs for its animation

sequences. Blocks until all the images have been loaded over

the Net OR one of the images reports a load error.

*/

public synchronized void LoadAnimationImages() {

           ImageLoaderThread = new ImageLoader[5];

           prefetchGC = createImage(1, 1).getGraphics();

 

           runningImages = createAgentImages( runninggifs );

           ImageLoaderThread[0] = new ImageLoader( this,runningImages,

                                                prefetchGC);

           resultImages = createAgentImages( resultgifs );

           ImageLoaderThread[1] = new ImageLoader( this,resultImages,

                                                prefetchGC);

           noResultImages = createAgentImages( noresultgifs );

           ImageLoaderThread[2] = new ImageLoader( this,

                                  noResultImages,prefetchGC);

           dispatchedImages = createAgentImages( dispatchedgifs );

           ImageLoaderThread[3] = new ImageLoader( this,

                                dispatchedImages,prefetchGC);

           notStartedImages = createAgentImages( notstartedgifs );

           ImageLoaderThread[4] = new ImageLoader( this,

                                notStartedImages,prefetchGC);

           boolean notloaded = true;

           while( notloaded )

                {

                if( ImageLoaderThread[0].allLoaded())

                     if( ImageLoaderThread[1].allLoaded())

                           if( ImageLoaderThread[2].allLoaded())

                                if( ImageLoaderThread[3].allLoaded())

                                     if( ImageLoaderThread[4].allLoaded())

                                           {

                                           notloaded= false;

                                           break;

                                           }

                try{

        wait(); }

      catch( InterruptedException e )

        {System.out.println( "interrupted" ); }

                }

  showStatus( "All images loaded!" );

}

 

 

/** Take an array of image file names and return

an array of AgentImages corresponding to those image

file names. Creates valid URLs from each image file name using

the URL that the applet was loaded from as the base, and

assuming that all the images are contained in a

subdirectory named "images."

@return An array of AgentImages with each member initialized

with the String URL of an image file, leaving the Image object

uninitialize and the loaded flag set to false.

*/

AgentImage[] createAgentImages( String file[] ) {

     int i;

 

     URL u = getCodeBase();

     String fn = new String(u.toString());

     StringTokenizer st = new StringTokenizer(fn,"/");

     String s = null;

     int count = st.countTokens();

     for( i = 0; i < count-1; i++ ) {

           if( s == null )

                s = new String( (String)st.nextElement());

           else

                {

                if( i == 1 )

                     s = new String(s+"//"+(String)st.nextElement());

                else

                     s = new String(s+"/"+(String)st.nextElement());

                }

           }

 

     String directory = new String( s+"/images/");

 

     AgentImage ag[] = new AgentImage[file.length];

     for( i = 0; i < file.length; i++ ) {

           System.out.println(directory+file[i]);

           ag[i] = new AgentImage( directory+file[i] );

           }

          

     return( ag );

     }

} // End the AgentLauncher class

 

 

/** Override of Component.imageUpdate, an implementation of

ImageObserver.imageUpdate. Called by an image producer every

time an interesting point in image loading occurs.

@param  img The image that we're talking about.

@param  infoflags The state of the image-load - a constant from

ImageObservers public variable set.

*/

public synchronized boolean imageUpdate(Image img,

          int infoflags, int x, int y, int width, int height)

     {

     System.out.println( "img "+img+" update" );

     if( infoflags == ERROR )

           bImageLoadingError = true;

     if( infoflags == ALLBITS || infoflags == ERROR )

           {

           ImageLoaderThread[0].setImageLoaded( img );

           ImageLoaderThread[1].setImageLoaded( img );

           ImageLoaderThread[2].setImageLoaded( img );

           ImageLoaderThread[3].setImageLoaded( img );

           ImageLoaderThread[4].setImageLoaded( img );

           }

     notify();

     return( true );

     }

 

}

 

 

/** A class to consolidate the loading of an array of images

into one place.

 

@version  1.2

@author John Rodley

*/

 

class ImageLoader extends Object {

AgentImage ai[];

AgentLauncher agl;

Graphics prefetchGC;

public boolean isFinished = false;

 

/** Copy the AgentImage array into a local array, then start

the image loading over the network by trying to draw it into

the Graphics object argument.

@param  g An off-screen graphics context that we can draw into.

Used simply to get Java to load the image over the net.

@param  al  An applet that we can call back into to update the

status line with image loading progress reports.

@param  a An array of structures that contain image file URLs

when passed in, and which we fill with Image objects.

*/

public ImageLoader(AgentLauncher al,AgentImage a[],Graphics g){

     ai = a;

     agl = al;

     prefetchGC = g;

     for( int i = 0; i < ai.length; i++ )

           {

    // If the image already exists in this array, simply copy

    // it. The previous copy will be loaded.

           ai[i].image = null;

           for( int j = 0; j < i; j++ )

                {

                if( ai[i].FileName == ai[j].FileName )

                     {

                     ai[i].image = ai[j].image;

                     break;

                     }

                }

           if( ai[i].image != null )

                continue;

 

    // Now make sure the URL is valid.

           URL u;

           try {

      u = new URL( ai[i].FileName );

      // The URL is valid, get the image.

           agl.showStatus( "Getting hollow image "+u );        

          ai[i].image = agl.getImage(u);

             boolean ret = g.drawImage(ai[i].image, 0, 0,al);

      }

           catch( MalformedURLException me ) {

                     agl.bImageLoadingError = true;

                     agl.sBadImage = new String( ai[i].FileName );

                     break;

                     }

           }

     }

 

/** Have all the bits for all the images in this array been

loaded over the network?

@return true if all the images have been loaded, false

otherwise.

*/

public boolean allLoaded() {

     for( int i = 0; i < ai.length; i++ )

           if( ai[i].loaded == false )

                return( false );

     return( true );

     }   

 

/** Look through this loaders image array and if this image is

in there, then set its loaded flag to true.

@param  img The Image object that has been reported completely loaded.

@return true if the image was in this loaders array, false

otherwise.

*/

public boolean setImageLoaded( Image img ) {

     boolean bret = false;

     for( int i = 0; i < ai.length; i++ )

           if( img == ai[i].image )

                {

                System.out.println( "image "+ai[i].FileName+" loaded");

                agl.showStatus( ai[i].FileName+" loaded" );

                ai[i].loaded = true;

                bret = true;

                }

     return( bret );

     }

}

 

 

/** This class is really just a struct, used to hold a gif's

URL, an Image corresponding to all the bits in that file, and a

flag that tells us whether the image has been loaded over the

Net or not.

*/

class AgentImage extends Object {

/** The String URL of the GIF. */

public String FileName;

/** The hollow Image object that gets filled in by

Applet.getImage. */

public Image image;

/** flag that is true if the image has been loaded over the

net, false if it has not been loaded yet. */

public boolean loaded = false;

 

/** Set the String URL. The loaded flag and the Image object

will be set by the users of this class using that String URL.*/

public AgentImage( String file_n ) {

     FileName = file_n;

     }

}

 

The first thing to consider is when in the applet’s execution trail do we want to load our images?  Obviously, because image loading involves a lot of time-consuming network traffic, we only want to do it once per applet execution. There’s only one method guaranteed to run only once, before the applet starts running—Applet.init. So that’s where we invoke our LoadAnimationImages method.

The next thing to look at is the inheritance chain for Applet, shown in Chapter 5 as Figure 5.1. Back among its superclasses is Component. If we look at the declaration of Component, we see that it implements the ImageObserver interface shown in Table 6.3.

Like many interfaces, ImageObserver requires a single method—imageUpdate. In Component, imageUpdate simply repaints the Component whenever it gets called. We will override it and use it for a little different purpose.

The next stop on our image loading journey is our own ImageLoader class. The purpose of ImageLoader is to consolidate the loading of an array of images into a single class. As such, we pass the constructor an AgentLauncher to use for updating the status line, an array of image file names, and a graphics context that it can draw those images into. The graphics context is an off-screen context which we’ll explain in a minute.

The ImageLoader constructor copies each image file name (provided by the AgentLauncher array instance variables notstartedgifs, resultgifs ...) into a local array and makes a URL out of it. If the URL is okay, then it calls back to Applet.getImage to retrieve a hollow Image object. getImage does not load the image over the Net. The next call, g.drawImage is the key to this operation. g is our off-screen graphics context, and calling drawImage against g causes the browser to actually go and fetch the image over the Net in a separate, asynchronous, image loading thread. drawImage returns immediately, usually with a negative return value. We’ve told the graphics context to draw our image but it has essentially said, “okay, I’ll get around to it.” We use an off-screen graphics context because we don’t really want these images to appear on the screen—we just want to load them over the Net.

The last argument to drawImage, an ImageObserver, is the hook that allows us to know for certain when our images have actually been loaded. We’ve passed our AgentLauncher (which implements ImageObserver via its superclass Component) as this argument. What will happen now that we’ve called drawImage, and passed it an ImageObserver, is that the asynchronous thread that loads images will call ImageObserver.imageUpdate whenever there is some progress in loading the image.

So now we have a mechanism in place where the image is loading over the network, and the image loading thread is calling one of our methods at regular intervals to tell us how it’s going. All we need to do now is force our main applet thread to block until the image loading thread has told us that all the images are loaded.

For this, we look to two places—our own LoadAnimationImages method, and our implementation of the ImageObserver.imageUpdate method. In LoadAnimationImages, we create one of our ImageLoader objects for each animation (array of images). When the ImageLoader constructor returns, the asynchronous image loading thread has started loading the images for that animation over the Net. So by the time we reach the while(notloaded) loop in LoadAnimationImages, the asynchronous image loading thread is busy downloading bytes over the Net.

The while loop asks each ImageLoader if all of its images have been loaded yet. If all the images for all the ImageLoaders have been loaded, the loop terminates. If they haven’t, we call Object.wait. What wait does is just that—wait. The thread in which wait was called blocks until it is notified by the aptly named method, Object.notify. So we reach the bottom of the while loop, and our main applet thread blocks in the call to wait.

Think about what is going on within the browser right now. The main applet thread is blocked, but the asynchronous image loading thread is still running. Not only is it running, but it is continually calling our imageUpdate method with progress reports on how the image loading is proceeding. The next step is to see what happens within imageUpdate.

The first argument to imageUpdate is the hollow Image object that the image-loading thread is trying to fill in. This argument tells us which image we’re talking about. The second argument, infoflags, is the key. This argument tells us what state the loading of this image has reached. If we go back to the definition of the ImageObserver interface in Table 6.3, we see a number of integer constants defined which represent the various stages of image construction. There are only two states that interest us—ERROR and ALLBITS. If there is an image loading error, infoflags==ERROR, we abort the whole process by setting bImageLoadingError. This is a bit abrupt, but suffices for now. If the image has been completely loaded, infoflags == ALLBITS, we tell the ImageLoaders to mark this image as loaded. Then we close the circle by calling Object.notify.

Remember what’s happening. The main applet thread is blocked in the call to wait, but the image-loading thread has invoked imageUpdate. When imageUpdate calls notify, the main applet thread wakes up and the call to wait returns. This is how wait and notify work; wait blocks and notify unblocks.

How does imageUpdate’s while loop terminate? The main applet thread is blocked in a call to wait. When the last bit of the last image has been loaded, the image-loading thread makes one last call to imageUpdate with infoflags set to ALLBITS. imageUpdate tells the ImageLoaders that this last image is done which means that ImageLoader.allLoaded will now return true. Then imageUpdate unblocks the main applet thread by calling notify. Our call to wait returns, we run through all the ImageLoaders asking them if all their images have been loaded via allLoaded. They return true, and the loop exits. At this point all the bits for all the images are now in memory on the user’s computer and we can call Component.drawImage with complete confidence that the image will actually appear on the screen.

In any animation sequence you get two choices: use predefined images, or draw the pictures yourself. In the AgentLauncher, we use a series of GIF files. The algorithm we’ll use is to define an animation as a series of images, then roll through that array displaying each image in turn in order to give the
illusion of movement.

Now the first question we have to confront with our animation is how “fine” we want our threading to be. Potentially, we could have many agents running, each getting an animation. Do we want each of those animations to be its own thread? We could do it that way but, on some systems, the overhead of even 15 or 20 additional threads could bring the system to a crawl. And the truth is, we can get the same effect by using a single new thread. In this case, the threaded object is a new subclass of PanelAnimationPanel. Listing 6.8 shows the AnimationPanel class.

Listing 6.8  The AnimationPanel Class

/** A class for containing, laying out and animating a

collection of animations. Lays animations out in a 10 by 10

grid, and animates by calling updateNextImage

against every animation in its collection.

*/

class AnimationPanel extends Panel implements Runnable {

/** The Vector of Animations */

     Vector a;

 

/** Constructor - Create a Vector for holding the animations,

and set us up as with a grid layout 10 by 10.

*/

  public AnimationPanel() {

    a = new Vector(1);

    setLayout( new FlowLayout());

    }

 

/** Add an animation to this panel. Adds the animation

Component to this Container, re-lays out the Container and adds

the animation to our Vector of animations. Called only when a

new agent reports in. Agent state changes are handled by

replaceAnimation.

@return The number of animations in this Panel.

*/

     public int addAnimation( Animation an ) {

           add( an );

    layout();

           int ret = a.size();

           a.addElement( an );

           return( ret );

       }

 

/** Replace the animation at the specified index with the

supplied animation. Removes the animations Component from this

Container, removes the animation itself from our Vector of

animations, hides the Component, shows the new animation and

re-lays out this Container.

*/

     public void replaceAnimation( int index, Animation ani ) {

           System.out.println( "replacing animation at "+index );

    Component c = (Component)a.elementAt(index);

    c.hide();

    remove(c);

    add( ani, index );

           a.insertElementAt( ani, index );

           a.removeElementAt( index+1 );

    ani.show();

    layout();

       }

 

/** Our thread's run method sits in an infinite loop, sleeping

for 1/3 of a second, then running through all the animations in

our Vector of Animations setting them to the next image in the

sequence and telling them to repaint.

*/

  public void run() {         

     Point pt = location();

     Dimension d = size();

       System.out.println( "size "+d.width+","+d.height);

     while (true) {

          for( int i = 0; i < a.size(); i++ )

                {

                  Animation ani = (Animation)a.elementAt( i );

                ani.updateNextImage();

                }

      try {

           Thread.sleep(300);

           } catch (InterruptedException e) {

           break;

      }

       }

     }

}

 

As you can see, the whole thing is deceptively simple. The single instance variable—a—is a Vector of animations. In fact, AnimationPanel serves only two functions: to collect all the animations in one place and to “flip” each animation to the next Image at regular intervals.

We lay the Panel out in a FlowLayout, via setLayout. This ensures that our animations will retain their preferredSize, and that they’ll line up in rows and columns as they get added to the Panel. When we want to add an animation to the Panel, we call addAnimation, which adds the Animation to the Panel itself, re-lays out the Panel and adds the animation to our Vector of animations. This is only called when an agent first checks in from the field.

replaceAnimation is used whenever an agent changes state; for instance, when it reports results. This removes the Component from our Container, removes the animation from our Vector of animations.

Whenever an Agent checks in from the field, we call addAnimation, supplying the animation that fits the current state of the agent. Each state—NOTSTARTED, DISPATCHED, RUNNING, RESULTS, NORESULTS—has an animation associated with it. One of these animations will be the current one for each agent in the field.

The Animation class, shown in Listing 6.9, is itself a subclass of Panel.

Listing 6.9  The Animation Class

/** Class representing an animation. Each animation is a

series of frames of consistent and constant size that get

displayed for a fixed amount of time. There are two types of

animations: one-shot and looping. One-shot animations run

once, looping animations run continually until they are

stopped from outside.

 

@author John Rodley

@version 1.4 March 10 1996

*/

 

class Animation extends Panel {

/** Is this animation actually animating now? */

  public boolean bRunning;

/** The images comprising the stages of the animation. */

  public AgentImage images[];

 

/** An agentface to call back to process the mouse click. */

  AgentFace af;

 

/** If this animation runs only one, this gets set to true. If this

is a loop that restarts at the beginning, set this to false. */

  public boolean bOneShot;

 

/** This index indicates which frame of the animation will be displayed

next. */

  public int FrameNumber;

  int width;

  int height;

 

/** An Applet for us to call back to. */

  Applet a = null;

 

/** Has this frame already been displayed?  Set false in

nextImage, set true in paint. */

  boolean bDisplayed = false;

 

/** Constructor - store the applet to call back to

@param  al  An applet to call back to.

*/

  public Animation(Applet al) {

     System.out.println( "empty animation for copying" );

       a = al;

     }

 

/** Create an animation that is not animating right now. */

  public Animation(Applet al, AgentImage im[], AgentFace agf ){

    // Save the applet and AgentFace objects.

     a = al;

    af = agf;

     bRunning = true;

       FrameNumber = 0;

     images = im;

    width = images[0].image.getWidth(a);

    height = images[0].image.getHeight(a);

    resize( width, height );

     }

 

/** This method tells the LayoutManager the smallest size we

can be. In our case, it's the size of the image we're

displaying.

@return A Dimension - width and height.

*/

  public synchronized Dimension minimumSize() {

    return( new Dimension( width, height ));

    }

 

/** This method tells the LayoutManager the size we prefer

to be. In our case, it's the size of the image we're

displaying.

@return A Dimension - width and height.

*/

  public synchronized Dimension preferredSize() {

    return( new Dimension( width, height ));

    }

 

/** Paint a single frame of this animation within an existing Graphics

context. */

  public void paint(Graphics g) {

    try {

         if( FrameNumber >= images.length )

               FrameNumber = 0;

 

    if( images[FrameNumber].image == null )

         {

               System.out.println( "no image" );

             return;

          }

    g.drawImage( images[FrameNumber].image, 0, 0, a );

      }

    catch( ArrayIndexOutOfBoundsException e ) {

      System.out.println( "badindex "+FrameNumber);

      }

     bDisplayed = true;

     }

 

/** Go to the next image. Increments FrameNumber, rewinding

back to 0 if we've gone over the edge of the array.

*/

  public void nextImage() {

     if( !bDisplayed ) return;

     if( FrameNumber >= (images.length-1) )

          FrameNumber = 0;

     else

          FrameNumber++;

     bDisplayed = false;

       }

 

/** Go to the next image and display it.

*/

  public void updateNextImage() {

     nextImage();

    repaint();

     }

 

/** Called if the user clicks the mouse in this Panel.

@return false

*/

  public boolean mouseDown( Event e, int x, int y ) {

    af.clicked();

       return( false );

     }

}

 

As you can see, Animation does the dirty work of actually drawing the image onto the screen. All it’s really designed to accomplish is to draw the proper Image, from an array of Images, into the graphics context whenever paint is called. The proper Image is indicated by the instance variable FrameNumber. In order to move along to the next Image in the sequence, our AnimationPanel calls updateNextImage at regular intervals. updateNextImage invokes nextImage to roll FrameNumber forward, then calls Component.repaint, which indirectly generates a call to paint.

preferredSize and minimumSize ensure that the AnimationPanel’s LayoutManager knows how much screen real estate to reserve for us.

paint actually draws the Image onto the screen. Because we use the paint method, rather than update, our Component is cleared before paint is invoked. The rest of the method is simple index checking and (possibly unnecessary) exception handling.

The last Animation method, mouseDown, is called whenever the mouse is clicked in our animation. mouseDown calls back to the AgentFace that owns it. Listing 6.10 shows the AgentFace class.

Listing 6.10  The AgentFace Class

/** A class that encapsulates the face that a dispatched agent

presents to the user. Contains a series of animations that

each represent a different possible state that the agent can be

in. At any time, only one of these animations will be running.

Clicking on the animation should take the user to the results

document for this agent. AgentFaces are uniquely identified by

the server name supplied to the constructor.

*/

class AgentFace extends Object {

  String StatusString = "";

  String serverName = "";

  Animation animation[];

 

  final int NOT_STARTED = 0;

  final int DISPATCHED = 1;

  final int RUNNING = 2;

  final int RETURNED_RESULTS = 3;

  final int RETURNED_NORESULTS = 4;

  AgentLauncher applet;

  String name;

  boolean bImageDisplay = true;

  public int index;

  public String url;

 

  int status = NOT_STARTED;

 

  public AgentFace( AgentLauncher ap, String ServerName, int index ) {

       name = new String( "AgentFace"+index );

     applet = ap;

       serverName = new String( ServerName );

     StatusString = new String( serverName+" Not started" );  

       animation = new Animation[5];

     animation[NOT_STARTED] = new Animation( ap,

                                  ap.notStartedImages, this );

       animation[DISPATCHED] = new Animation( ap,

                                  ap.dispatchedImages, this );

     animation[RUNNING] = new Animation( ap,

                                  ap.runningImages, this );

     animation[RETURNED_RESULTS] = new Animation( ap,

                                  ap.resultImages, this);

     animation[RETURNED_NORESULTS] = new Animation( ap,

                                ap.noResultImages, this );

       }

 

/** Called by the animation whenever the user clicks in the

animation. If the agent has finished work and has something to

report, this takes us over to that document, else it displays a

message box explaining what state the agent is in.

*/

  public boolean clicked() {

     String msg;

       MessageBox m;

 

       Frame f = ComponentUtil.getFrame(applet);

     switch( status ) {

          case NOT_STARTED:

                msg = new String( "Not started." );

                  m = new MessageBox( f, msg );

                m.ShowAndLayout();

                break;

             case DISPATCHED:

                  msg = new String( "Not running on server "+serverName+" yet." );

                m = new MessageBox( f, msg );

                m.ShowAndLayout();

                break;

           case RUNNING:

                msg = new String( "Server "+serverName+" not finished yet." );

                m = new MessageBox( f, msg );

                  m.ShowAndLayout();

                break;

          case RETURNED_RESULTS:

                System.out.println( "Switching to URL "+url );

        AppletContext ac = applet.getAppletContext();

        ac.showDocument( url );

                  break;

           case RETURNED_NORESULTS:

                msg = new String( "Agent on server "+serverName+

                                    " didn't find anything." );

                m = new MessageBox( f, msg );

                  m.ShowAndLayout();

                break;

          }        

     return( true );

       }

 

/** Changes the AgentFace's state to dispatched. */

  public void dispatched() {

     status = DISPATCHED;

       StatusString = new String( serverName+" Dispatched" );

  }

/** Changes the AgentFace's state to running. */

  public void running() {

     status = RUNNING;

     StatusString = new String( serverName+" Running" );

  }

/** Changes the AgentFace's state to returned with results. */

  public void returnedResults(String u) {

     url = new String(u);

       status = RETURNED_RESULTS;

     StatusString = new String(serverName+" Finished Results");

     }

/** Change the AgentFace's state to returned with no results.*/

  public void returnedNoResults() {

     status = RETURNED_NORESULTS;

       StatusString =

      new String( serverName+" Finished NO Results" );

  }

 

/** Return the animation that represents this agent's current

state.

@return An Animation.

*/

  public Animation getCurrentAnimation() {

       return( animation[status] );

     }

}

 

The point of AgentFace is to encapsulate the set of possible animations, and to keep track of which one is current for a particular agent. There is one AgentFace per agent and each one is uniquely identified by the server name of the server the agent is running on. Listing 6.11 shows the reportResult method that changes the animation via the AgentFace’s status methods.

Listing 6.11  ­Changing the On-Screen Animation

/** A method called by the message processing code

(AgentDispatcher) to change an agents state from running to

finished with or without results.

@param  url The URL of the results file. If null, there were

no results.

@param  server  The name of the server. We find the AgentFace

we need to change by searching on this name.

*/

  public void reportResult( String server, String url ) {

       for( int i = 0; i < ALVector.size(); i++ )

           {

          AgentFace af = (AgentFace)ALVector.elementAt(i);

             if( af.serverName.compareTo(server) == 0 )

                  {

                if( url != null )

                      af.returnedResults(url);

                  else

                       af.returnedNoResults();

                p.replaceAnimation( af.index, af.getCurrentAnimation());

                return;

                }

           }   

     }

 

As it receives messages from an agent detailing the agent’s progress, the AgentLauncher calls one of the status change methods in AgentFace (running, dispatched, returnedResults, or returnedNoResults), then it replaces the old animation with one that represents the new state. Listing 6.1 shows the AgentLauncher code (via AgentDispatcher) that calls reportResult upon receipt of a ResultMessage. This process completes the chain we’ve tried to build from the agent working out on a remote server, back to an animation running on-screen in front of the user who dispatched the agent. Messages come in over the socket, the AgentDispatcher calls into the AgentLauncher to change the AgentFace’s state, and the animation representing that agent changes.

Native Methods

The first thing I always look at when reviewing a new language like Java is how to get out of it. This is only natural, as most programmers are already productive in one language and most projects need some of the functionality of last year’s language in the new product they’re writing in next year’s language. Windows GUI development languages, like Easel or Toolbook, usually provide the ability to make DLL calls and Java provides an analogous escape hatch—native methods.

Native methods allow you to link dynamic libraries written in C, C++, or some other language via a method “wrapper.” Basically, if your DLL call obeys certain rules about parameter passing, memory management, and such, you can write a method that simply calls out to that external DLL. Native methods are crucial for shrink-wrapped, standalone Java applications where your program is competing against code written to the native operating system API. Native methods, however, are not useful for the purposes of our application for two reasons. First and most important, by definition a native method is not portable and one of the base requirements for our application is that it run in any Java-capable browser. Second, and perhaps not so obvious, is the whole question of how you would get your native method onto a user’s machine. The Java classloader is designed for moving java .class files, not Windows .DLL files. Thus, you could write the world’s coolest native method, but any Java-capable browser trying to run your applet over the Net would throw an exception when it tried to (locally) load your native method.

Conclusion

Java makes threading simple and worthwhile. You can write Java applets the same way you used to write single-threaded applications in C or C++, but you’d be wasting much of the power of the language. The current generation of browsers are all multithreaded, and threading will only become more prevalent as a method of increasing the real and apparent efficiency of applications.