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

I'm going to start off this month on a deliberately alarmist note. Feel free to skip to the next section if my foaming at the mouth starts to bother you. Bear in mind, as you read this, that I'm one of the "good guys." I think Java is the best thing since sliced bread. In fact, I've abandoned C++ for Java. Virtually all of my work nowadays centers around Java programming -- either doing it or teaching it. Nonetheless, I'm hoping that some public criticism, as in the following, might encourage Sun to operate in a more responsible fashion than is described here.

Whatever happened to 'run anywhere'?

So, what's so upsetting? It seems Sun has abandoned the "run anywhere" part of its oft-quoted write-once, run-anywhere mantra. I'll explain. The ability to "run anywhere" is predicated on a class file being able to run on any machine that has a Java virtual machine (JVM) on it, and on that VM being able to run any class file it's given. If different implementations of the JVM recognize different class file formats, platform independence is a pipe dream. Microsoft understands this issue very well. That's why it tried to push a nonconforming VM onto Windows. Programs that leverage Microsoft's nonstandard extensions simply are not platform-independent.

This standardization is particularly important in a network environment, where an application installed on a server and distributed automatically must be able to run on all the client boxes, regardless of the VM that's installed on a given box. You can always download a new library -- that's one of the reasons Swing is in the javax (rather than java) package. Updating the VM itself is another matter. Sun is well aware of this issue. James Gosling himself has often justified changes to the language at the source-code level by saying that no VM changes would be mandated.

In the face of this fact, I was horrified to find that the Java 2 platform (formerly known as JDK 1.2) compiler can create a nonstandard class file format recognizable only to the Java 2 VM. The class file generated by java -target 1.2 will not run on a 1.1 VM. Since you have absolutely no control over which version of the VM is on the client platform, using this switch effectively throws away any hope of platform independence. You'll have to ship your Java application with a version of the Java 2 VM for every platform on which you'd hope to run, and we're back in DLL hell.

Fortunately, the switch is off by default, and you don't have to use it. But even the presence of this switch indicates that Sun is perhaps not as trustworthy as the Java community has been assuming. At a recent Java SIGs meeting hosted by San Jose's Software Development Forum, Gosling stated that even though Java has maintained backward compatibility as it has evolved, Sun has no commitment to this compatibility. He implied that there would eventually come a time where we would all be forced to adopt the new class file format. I can see it now: a Microsoft PR guy waving around a disk, shouting "Here's an official Java application, compiled with Sun's own Java compiler, and it won't run on these Windows, Solaris, or Mac boxes unless you spend hours upgrading an obscure piece of software called the virtual machine. In fact, there's no guarantee that you can even get such an upgrade if you're not running Solaris. Platform independence? Ha!"

The fact that the new nonstandard class file format was sprung on the Java community with no advanced warning or feedback seems to me the worst problem of all. There must be a way to move forward, of course. It's inevitable that the VM spec will change as Java evolves. However, this change should come about through an open process that involves the whole Java community, not a unilateral move on the part of Sun. This no-questions-asked imposition of a new standard strikes me as a betrayal of Sun's commitment to Java as an open standard, and calls Sun's legitimacy as a standards-setting body into question. It seems silly to gripe about Microsoft creating a noncompliant compiler/VM when Sun itself has now done exactly that. I suppose I've been deluding myself into thinking Sun was acting as the steward of an open standard. It turns out that Sun owns the standard, and like Microsoft, does what it feels like whenever it feels like doing it.

Frankly, I can't imagine what would be so important that Sun would be willing to change the VM spec at this point in time. As far as I can tell from experimenting, the generated bytecode is identical to the 1.1 bytecode except for the new version number. (This one difference is enough to prevent a 1.1 VM from loading a Java 2 class file, however.) Speaking as a compiler writer, there are a few annoyances in the bytecode -- mainly workarounds for VM bugs -- but these are trivial matters. I've asked both Gosling and Tim Lindholm (the latter is one of the coauthors of the VM) why the change was made, but neither has given me an answer. Perhaps this ploy is intended solely to render the Microsoft VM irrelevant, since it won't conform to the Java 2 spec. If one of you readers can enlighten me, please send me e-mail; my address is available in the bio below.

Back to threads

On a more prosaic (or perhaps Prozac) note, let's get back to the subject at hand: threads. This month, I want to talk about timers -- objects that help execute some operation at a fixed interval. I'll discuss both the timer that's part of the Swing package and a roll-your-own timer that I've found more useful in some situations.

It's often the case that programs need to execute some operation at a regular interval. Animation loops come to mind immediately, but other applications are legion. Consider the case of a thread that must notify a large list of observers that some event has occurred. Using a timer to notify one observer from the list on each "tick" of the timer can give other threads a chance to run while notifications are in progress.

A simple timed wait doesn't often do the trick, though. For example, each loop iteration in the following example could takes a different amount of time to execute, depending on which way the test goes and how long the database server takes to process requests:

    while( running )
    {
        if( some_condition )
            notify_any_observers( FAILED );
        else
        {
            open_a_database();
            make_1000_queries();
            process_the_results():
            generate_a_billion_reports();
            notify_any_observers( SUCCESS );
        }
        wait( FIXED_INTERVAL ); // There is no matching notify(). Just time out.
    }

Swingin' threads

The first solution to this problem is the new javax.swing.Timer class. The Timer is designed to be a Swing-specific solution (it's only available if Swing is installed on the client system) to a Swing-specific problem: Swing itself is not thread-safe.

The thread-safety issue is an important one. Swing's threading support is described in depth in a Tech note on the Sun Web site, but to summarize: Swing, like AWT before it, uses a single thread to field all user interface (UI) events that come in from the operating system. This thread dequeues OS events from some sort of event queue, figures out what the OS is trying to tell it, and then notifies any listeners interested in the event. For example, the event thread could dequeue a WM_MOUSEMOVE message from the Windows message queue, and then send out mouseMoved() messages to all the interested MouseMotionListener objects.

These event handler methods (such as mouseMoved()) actually run on the thread that's dequeueing the OS-level events. This single-threaded approach is a big issue, because the UI is effectively locked while the event handler messages are executing. That is, no OS-level messages (such as button presses) are serviced while listener notifications are in progress. To keep your UI responsive to user input, these event handler functions should be very short and very fast. They must spawn off threads to do time-consuming operations.

To make matters more interesting, none of Swing class's methods are thread safe -- they are simply not designed to handle the case of two threads accessing them simultaneously. The performance hit required to make Swing thread-safe (not to mention the extra complexity) is just unacceptable. The lack of thread safety means that once you start up the Swing event handler thread (by calling setVisible(), pack(), or any other method that makes a window visible), you cannot safely call a Swing method from anywhere but Swing's own event handler thread. (Note that you can safely manipulate Swing objects before the event handling thread starts -- before you call setVisible() or pack(), for example.)

The lack of thread safety is often not an issue because Swing methods are, more often than not, called from Swing's own event handler thread -- remember, all the listener methods run on that thread. For example, a listener's event handler like mouseMoved() might pass a repaint() message to some Component. This operation is safe because mouseMoved() is running on Swing's own thread.

The invokeLater() and invokeAndWait() methods

There are often cases where some thread of your own devising needs to communicate with Swing, however. For example, the main thread of an application could fire up a window, do some stuff, then want to modify the window. Since you obviously need to pass messages to Swing at times other than initialization time, a mechanism has been provided to ask the Swing event-handling thread to execute a block of code for you. You need to encapsulate your code into a Runnable object, and then pass that object to Swing by calling SwingUtilities.invokeLater() or SwingUtilities.invokeAndWait(). For example, a thread could force a window to redraw itself like this:

    Component some_window = ... ;
    Swingutilities.invokeLater( new Runnable()
                               {   public void run()
                                    {   some_window.repaint();
                                    }
                                }
                              );

The asynchronous invokeLater() message just puts the Runnable object onto the event queue and then returns. The object's run() method is executed when Swing gets around to it. The invokeAndWait() method blocks until the run() function has been executed. Since the associated run() methods run on the Swing event handler thread, synchronously with other Swing methods, the thread safety of the individual Swing methods is irrelevant.

Don't be misled by the fact that you're using a Runnable object to pass a request to Swing. The run() method doesn't run on its own thread; it uses the Swing event queue. If run() takes a long time to execute, your program's user interface will appear to lock up -- it won't respond at all to any user input. It's best for these methods either to be very short or to fire up their own threads to do time-consuming background operations.

Using the Swing Timer

One common situation where a standalone thread needs to talk to Swing is an animation loop. Listing 1 below shows how you might be tempted to implement an animation loop. This is typical AWT-style code, where the run() method of a thread (line 42) forces a repaint at a regular interval. The thread is fired up in start() (line 23). Unfortunately, this code only appears to work -- it will probably run fine until you try to actually use an Animator1 object in a real application that uses Swing intensively, then it will blow up. The run() method on line 42 of Listing 1 runs on the thread created on line 24, not on the Swing thread. Consequently, the call to repaint on line 51 is not safe. We're calling an unsynchronized Swing method from somewhere other than a listener method or the Swing event processing thread.

Also, the animation loop is a bit jerky because the repaint doesn't actually happen on a fixed interval. There's a difference between repainting every 100 milliseconds and waiting 100 milliseconds between repaint requests. In the latter case, which is what I'm doing here, the actual time between refreshes can vary, depending on how long it takes for repaint() to execute. That's not really the behavior I want.

Listing 1. (Animator1.java) The wrong way to do animation
1 2 3 4 5 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more