Programming Java threads in the real world, Part 5

Has Sun abandoned 'run anywhere'? Plus: Threads and Swing, timers, and getting around stop(), suspend(), and resume() deprecation

1 2 3 4 5 Page 2
Page 2 of 5
01  
02  
03  
04  
05  
06  
07  
08  
09  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
82  
83  
84  
import java.awt.*;
import javax.swing.*;
public class Animator1 extends JComponent implements Runnable
{
   private final int     refresh_rate;
   private final Image[] frames;
   private       int     current_frame = 0;
   private       Thread  driver;
    //---------------------------------------------------
   public Animator1( Image[] frames, int refresh_rate )
                                            throws InterruptedException
    {
        this.frames       = frames;
        this.refresh_rate = refresh_rate;
        MediaTracker tracker = new MediaTracker(this);  // wait for all
        for( int i = 0; i < frames.length; ++i )        // images to 
            tracker.addImage( frames[i], 0 );           // load
        tracker.waitForAll();
    }
    //---------------------------------------------------
   public void start()
   {   driver = new Thread( this );
        driver.start();
    }
    //---------------------------------------------------
   public void stop()
    {   driver.interrupt();
    }
    //---------------------------------------------------
   public Dimension getPreferredSize()
    {   return new Dimension(   frames[0].getWidth(this),
                                frames[0].getHeight(this) );
    }
    //---------------------------------------------------
   public void paint( Graphics g )
    {   g.drawImage( frames[ current_frame ], 0, 0,
                        getBounds().width, getBounds().height, this );
    }
    //---------------------------------------------------
   public void run()
    {   try
        {   while( !Thread.interrupted() )
            {
                Thread.currentThread().sleep( refresh_rate );
                ++current_frame;
                current_frame %= frames.length;
               Animator1.this.repaint();
            }
        }
        catch( InterruptedException e ){/*terminate thread by returning*/}
    }
    //==================================================================
   public static class Test extends JFrame
    {
       static public void main( String[] s ) throws Exception
        {
            Toolkit kit     = Toolkit.getDefaultToolkit();
            Image[] images  = new Image[]
            {
                kit.getImage("5.images/Juggler0.gif"),
                kit.getImage("5.images/Juggler1.gif"),
                kit.getImage("5.images/Juggler2.gif"),
                kit.getImage("5.images/Juggler3.gif"),
                kit.getImage("5.images/Juggler4.gif") 
            };
            Animator1 animator = new Animator1( images, 100 );
            JFrame frame = new Test();
            frame.getContentPane().add( animator );
            frame.pack();
            frame.show();
            animator.start();
            Thread.currentThread().sleep( 5 * 1000 );
            animator.stop();
            System.exit(0);
        }
    }
}

Killing threads

You should pay attention to how run() terminates in Listing 1. Jeff Richter, in his book, Advanced Windows Programming, says that there are three ways in which threads can die: they can die a natural death, commit suicide, or be murdered! Java's stop() method, use of which falls in both the murder and suicide category, is now deprecated because it can leave NT DLLs in an unstable state, so "murder" and "suicide" aren't options. The remaining option, a "natural death," is brought about by returning from run(). If you want to shut a thread down, you have to ask the thread to shut itself down.

A simple approach might simply call a method that sets an internal flag that's tested in the main run() loop of the thread. This approach doesn't usually work, however, because most threads spend the bulk of their time blocked, waiting for some operation like a blocking read to complete. A suspended thread can't test a flag. However, sending a blocked thread an interrupt() message will bump it out of the wait (with an exception toss). The stop() method (line 28) does just that: it sends an interrupt() message to the driver thread in order to stop it. (Remember, stop() itself runs on whatever thread calls it, not on the animator thread. The only methods that run on the animator thread are the ones that Animator.run() calls.) Since I intend for an interrupt operation to kill the thread, the run() method (line 42) catches the exception and ignores it.

What if the thread is interrupted, rather than blocked, while it's working? The Thread class provides two methods to check whether or not a thread has been interrupted. Thread.interrupted(), which I've used here, returns true if the current thread has been interrupted. A call to some_thread.isInterrupted() returns true if some_thread has been interrupted. It's a good idea to call one or the other of these periodically as the thread works.

Unit tests

The final thing to look at in Listing 1 is the unit-test class: Test, on line 57. A unit test is a piece of code that tests one thing, in this case a class. It's a good idea to associate a unit test with every class you write, and there are several ways to do that. You could pair a test file with every class file, but you'd end up with too many files to maintain if you did that, and keeping the test routine in phase with the class as it evolves would be difficult since you could recompile the class without recompiling the test.

Another strategy is to put a main() in every class. I don't do that because I don't want to carry around the baggage of a unit test in every class file that comprises the program. The tests are often bigger than the class I'm testing, and I don't want to inflate the application size with all this test code.

A third option -- putting the test code in a separate class, but in the same file as the class being tested -- doesn't work out very well because the package-level name space becomes corrupted with all the names of the test classes. Just managing these names can become a big problem.

The strategy I eventually hit on is to test a class using an inner class of the class I'm testing. I always call this class Test. This inner class resides in its own class file, and I don't ship it with the application, so it doesn't increase the application's size. The test class's name is within the name space of the outer class, so the global name space isn't corrupted with stuff the user of the class couldn't care less about. Given a set of classes listed in a makefile, I can easily run a whole battery of tests from a makefile by deriving the test class name from the source file name using macros. For example, I can test the current class by executing:

java Animator1\$Test

(You don't need the backslash on Windows machines.) You can't write java Animator.Test because the VM will think you're trying to run the Test class in the Animator package.

Use Timer

Getting back to animations, Swing solves the it's-not-safe-to-call-a-Swing-method-from-another-thread problem with the Timer class, which executes requests on Swing's own event processing thread at fixed intervals. I think of this kind of timer as "proactive." You set up a timer interval and register one or more ActionListener objects with it. The Timer then sends an actionPerformed() message to each listener when the interval elapses. (All the listeners are notified at once, not one per interval.) These notifications are executed from the Swing event loop, so they can happily call Swing methods without worrying about thread synchronization.

Listing 2 below demonstrates how to use a timer by printing the time once per second. The Timer is initialized with a time interval (1000 milliseconds) and is provided an ActionListener to notify when the interval expires. The listener's actionPerformed() method both prints the current time and advances the elapsed-time counter. After five seconds (and five calls to actionPerformed()), the condition variable (which I discussed last month) is set to true, thereby releasing any threads that are waiting for the timer to finish. The main thread creates a Swing_timer, the constructor of which starts up the clock, then blocks, waiting for the done condition to become true. The net effect is that the constructor doesn't return until five seconds have elapsed.

Sun's Timer is a bit more flexible than I've shown; it can also work in one-shot mode, where the listeners are notified only once, for example. The Timer also supports methods that let you add additional listeners, change the timer interval, and so forth.

Listing 2. (Swing_timer.java) Using javax.swing.Timer
01  
02  
03  
04  
05  
06  
07  
08  
09  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
import javax.swing.*;
import java.util.*;
import java.awt.event.*;
import com.holub.asynch.Condition; // Discussed last month.
public class Swing_timer
{
   private Condition done          = new Condition(false);
   private int       elapsed_time  = 0;
    // Create a one-second timer:
    Timer clock = new Timer( 1000,
                        new ActionListener()
                       {   public void actionPerformed( ActionEvent e )
                           {   synchronized( Swing_timer.this )
                                {   System.out.println( (new Date()).toString() );
                                    if( ++elapsed_time == 5 )
                                        done.set_true();
                                }
                            }
                        }
                    );
    Swing_timer() throws InterruptedException
    {   clock.start();
       done.wait_for_true();
    }
   public static void main(String[] args) throws InterruptedException
    {   new Swing_timer();
    }
}

So, how does it work?

It's interesting to delve into the inner workings of Swing's Timer. (The sources are in the JDK's src.jar file, installed in the JDK root directory if you tell the installer program to install it.)

You would expect a timer to fire off a thread whose run() method would first wait for the time interval to elapse, then notify listeners. This thread would then either terminate or loop back up and wait again, depending on the timer's mode. That's more or less how my Alarm class (which I'll present in a moment) works, and that's how our failed attempt at an animation loop worked as well.

Swing's Timer doesn't work this way, however. What you don't see on the surface is a second package-access class, called a TimerQueue. This class is a Singleton -- only one instance of it exists. It's the TimerQueue object that encapsulates the "wait" thread. The Timer object actually doesn't do any timing at all. It just stores its time interval internally. When you start it up, the Timer object adds itself to the TimerQueue. The queue keeps the timers ordered by expiration time, and puts itself to sleep until that time arrives. The TimerQueue object then passes the ball back to the expired timer, asking it to notify its listeners.

At this juncture, the Timer object calls Swing's invokeLater() method (discussed earlier) and passes it a Runnable object whose run() method sends ActionPerformed() messages to the timer's ActionListener objects. It's important to observe that this notification happens on Swing's event processing thread, so the ActionPerformed() method can happily call Swing functions without worrying about thread safety.

By default, if several references to the same timer object are waiting in the notification queue, all but one are ignored. (Notification requests can build up in this way if the application is very busy servicing normal OS-event handlers.) This way, a listener doesn't get multiple notifications coming one on top of the other. This behavior can be suppressed by passing the Timer object a setCoalesce(false) message.

In Listing 3, below, I've rewritten the earlier animation example to use a Swing Timer. The new code is in boldface, and the main difference between this version and the previous one is the replacement of the run() method with an actionPerformed(), which is invoked from the Swing event thread when the timer expires.

Listing 3. (Animator.java) Animation with a Swing Timer
01  
02  
03  
04  
05  
06  
07  
08  
09  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
43  
44  
45  
46  
47  
48  
49  
50  
51  
52  
53  
54  
55  
56  
57  
58  
59  
60  
61  
62  
63  
64  
65  
66  
67  
68  
69  
70  
71  
72  
73  
74  
75  
76  
77  
78  
79  
80  
81  
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Animator extends JComponent implements ActionListener //
{   Timer   timer;
   private final int     refresh_rate;
   private final Image[] frames;
   private       int     current_frame = 0;
   private       Thread  driver;
    //---------------------------------------------------
   public Animator( Image[] frames, int refresh_rate )
                                            throws InterruptedException
    {
        this.frames       = frames;
        this.refresh_rate = refresh_rate;
       timer= new Timer(refresh_rate, this);
        timer.setInitialDelay(0);
        timer.setCoalesce(true);
        MediaTracker tracker = new MediaTracker(this);  // wait for all
        for( int i = 0; i < frames.length; ++i )        // images to 
            tracker.addImage( frames[i], 0 );           // load
        tracker.waitForAll();
    }
    //---------------------------------------------------
   public void start() //
    {   timer.start();
    }
    //---------------------------------------------------
   public void stop()      //
    {   timer.stop();
    }
    //---------------------------------------------------
   public Dimension getPreferredSize()
    {   return new Dimension(   frames[0].getWidth(this),
                                frames[0].getHeight(this) );
    }
    //---------------------------------------------------
   public void paint( Graphics g )
    {   g.drawImage( frames[ current_frame ], 0, 0,
                        getBounds().width, getBounds().height, this );
    }
    //---------------------------------------------------
   public void actionPerformed( ActionEvent e )
    {   ++current_frame;
        current_frame %= frames.length;
        repaint();
    }
    //==================================================================
   public static class Test extends JFrame
    {
       static public void main( String[] s ) throws Exception
        {
            Toolkit kit     = Toolkit.getDefaultToolkit();
            Image[] images  = new Image[]
            {
                kit.getImage("5.images/Juggler0.gif"),
                kit.getImage("5.images/Juggler1.gif"),
                kit.getImage("5.images/Juggler2.gif"),
                kit.getImage("5.images/Juggler3.gif"),
                kit.getImage("5.images/Juggler4.gif") 
            };
            Animator animator = new Animator( images, 100 );
            JFrame frame = new Test();
            frame.getContentPane().add( animator );
            frame.pack();
            frame.show();
            animator.start();
            Thread.currentThread().sleep( 5 * 1000 );
            animator.stop();
            System.exit(0);
        }
    }
}

Why use a Swing Timer?

The main advantage to Swing's approach (over and above the thread-safety issue) is that the number of threads that exist at a given moment is small, since all the real work is done on the Swing event thread, and all timers share the same TimerQueue.

There are lots of disadvantages, though:

1 2 3 4 5 Page 2
Page 2 of 5