The Last
Word in Swing Threads
Working with Asynchronous
Models
By Joseph Bowbeer
This
is not an article in any particular series, but it is the third
article that The Swing Connection has published on the use of threads
in Swing.
The first article, Threads
and Swing, explained Swing's single-thread rule. It now
resides in The Swing Connection Archive.
The second article, Using
a Swing Worker Thread, demonstrated the use of a SwingWorker
thread utility class. It has also now been archived.
This article introduces a revised SwingWorker class, and demonstrates
two methods for using threads with model-based components like JTable
and JTree.
To understand the material presented in this article, it helps
to have some familiarity with SwingWorker and the JTable and JTree
components. You can find articles on the JTable and JTree components
in the Tech Topics section,
and also in The Swing Connection Archive.
For more information about resource materials, see the Resources
section at the end of this article.
This article is divided into the following
major sections:
Introduction and review
Dynamic tree
Remote table
SwingWorker revised
Downloading
Conclusion
Resources
About the author
Introduction
and Review
Before delving into trees and tables and asynchronous models, I
want to first review Swing's single-thread rule and examine its
implications.
Swing's single-thread rule says that
Swing components can be accessed by only one thread at a time.
This rule applies to both gets and sets, and the
single thread is generally the event dispatch thread.
The single-thread rule is a good match for UI components because
they tend to be used in a single-threaded way anyway, with most
actions being initiated by the user. Building thread safe components
is difficult and tedious: it's a good thing not to be doing
if it can be avoided. But for all its benefits, the single- thread
rule has far-reaching implications.
Swing components will generally not comply with the single-thread
rule unless all their events are sent and received on the event-dispatch
thread. For example, property-change events should be sent on the
event-dispatch thread, and model-change events should be received
on the event-dispatch thread.
For model-based components such as JTable and JTree, the single-thread
rule implies that the model itself can only be accessed by the event-dispatch
thread. For this reason, the model's methods must execute quickly
and should never block, or the entire user interface will be unresponsive.
But suppose you have a JTree with
a TreeModel that accesses a remote server?
You may find that when the server is unavailable or is
bogged down, the calls to your TreeModel block, freezing the entire
user interface. What can you do to make the interface more responsive?
Or suppose you have a JTable with
a TableModel that manages a device on the network?
You may find that your interface crawls when the managed
device is busy or when the network is congested. What can you do?
These situations call for threads. And when the need arises, there
are several ways to use threads with Swing, despite the single-thread
rule.
This article presents two methods for using threads to access slow,
remote, or otherwise asynchronous models, and demonstrates
their use with the JTree and JTable components. (A model that is
not accessed solely by the event- dispatch thread is said
to be asynchronous with respect to that thread.)
Dynamic Tree
Suppose you have a JTree with a TreeModel that accesses a
remote server, but the server is slow or unreliable?
DynamicTree demonstrates the use of a background thread to dynamically
expand the nodes of a JTree.
Figure 1 shows DynamicTree in the process of expanding a node.
Figure 1.
Split-model design
DynamicTree is based on a split-model design. In this design, the
real model is the asynchronous model, which may be slow
or unreliable. The JTree component uses a standard, synchronous
model to maintain a snapshot of the real model. (The asynchronous
model may reside on a remote server and for this reason I often
refer to it as the remote model, and I refer to the Swing
component's model as the local model.)
Using a local model to shadow or cache the remote model has the
advantage of providing a consistent, responsive model to the Swing
component at all times. A disadvantage, one of the tradeoffs, is
the duplication of model data. Another problem is that the two models
are not always in synch, and they must be coordinated in some way.
Update as expanded: dynamic browsing
One way to coordinate the models is to update the local model only
as the data is needed, and not before. This technique is useful
when the remote model is slow or very large, but is basically static.
DynamicTree uses this method to browse a slow, static tree model.
DynamicTree starts out as an unexpanded root node, while an expansion
listener waits for the user to expand a node. When a node is expanded,
the expansion listener starts a SwingWorker thread. The worker's
construct() method accesses the remote model
and returns new children for the expanded node. Then the worker's
finished() method, running
on the event-dispatch thread, adds the children to the local model.
For simplicity, only one worker is permitted to run at a time.
When any node is collapsed, the active worker, if any, is interrupted.
(No effort is made to see if the collapsed node is an ancestor of
the node the worker is expanding.)
Execution sequence
Figure 2 shows the node expansion process. Execution begins and
ends in the event-dispatch thread, on the left. SwingWorker execution
begins on the right, in the worker's thread. The solid arrows are
method calls, the dashed arrows are returns, and the half arrows
indicate asynchronous invocation.
Figure 2. DynamicTree execution sequence.
The rest of this section describes the class structure and implementation.
Read on or skip ahead to the remote
table demo. The Downloading section explains how to download and
run this demo.
Implementation
Figure 3 shows a rough diagram of the class structure.
Figure 3. DynamicTree classes.
The local model is a DefaultTreeModel containing DefaultMutableTreeNode
nodes. The nodes must be mutable in order for their children to
be added dynamically. The mutable node's userObject
is a handy place to store a link back to the remote model.
TreeNodeFactory
The remote model implements the TreeNodeFactory interface:
public interface TreeNodeFactory {
DefaultMutableTreeNode[] createChildren(Object userObject)
throws Exception;
}
The createChildren() method
is called in the worker thread. Like most asynchronous or remote
methods, it may throw an exception (typically a RemoteException
or an InterruptedException). The userObject
parameter is the newly-expanded node's link back to the remote model.
Returning all the children in an array is more efficient than returning
each child individually, and avoids dealing with partial failure.
Initially, each child node is assigned an allowsChildren property and a link
back to the remote model. (The allowsChildren property is set false
if the remote node is a leaf; otherwise it is set true to indicate that the node can
be expanded.)
DefaultTreeNodeFactory is an Adapter (see Design Patterns
by The Gang of Four).
It adapts any TreeModel to the TreeNodeFactory interface. The default
tree model is used in the demo. Some commented-out code in the main
method installs a FileSystemNodeFactory. SlowTreeNodeFactory is
a wrapper used for simulation purposes; it inserts random delays.
Future work
I've tried to keep DynamicTree simple. The nodes have no content,
besides their name. In a situation where the nodes have substantial
content, it might be helpful to load the content asynchronously,
perhaps using a tree selection listener to initiate the loading.
The remote table demo in the next section is more sophisticated.
Remote Table
Suppose you have a JTable with a TableModel that manages
a remote device, but the interface really crawls when the device
is busy?
The remote table demo uses background threads and asynchronous
callbacks to view and modify a remote table model.
Figure 4 shows a remote table editor sending a new value to the
server. The pending cell will remain yellow until the update is
complete.
Figure 4.
RemoteTable bean
The remote table demo is implemented with RMI and consists of a
server and two clients: an editor and a viewer. The viewer is actually
just an editor with its editing capability disabled.
The clients use a RemoteTable component bean, which is a subclass
of JTable designed to work with remote table models. The editor
shown in Figure 4 consists of a RemoteTable component, a status
label, a simple activity meter (bottom right), and some code to
locate the server.
Update when notified
Unlike DynamicTree, which updates its local model only when data
is needed, the RemoteTable component begins by fetching all the
data, and then listens for subsequent changes. This notification-based
technique can be used with dynamic models, and works best when the
models are fairly small.
Cell editing drives the notification. When the user finishes editing
a cell, RemoteTable marks the edited cell with a pending value (yellow
highlight) and schedules a SwingWorker task. The worker's construct() method sends the new value to the
remote model.
When the remote model receives the new value, it notifies its listeners
of the change. The worker's finished() method does nothing more
than indicate the task has completed; the yellow pending cell turns
back into a normal cell when notification from the remote model
is received and processed.
Task queue
RemoteTable schedules its SwingWorker tasks on a QueuedExecutor,
which runs the tasks sequentially on a single thread. (QueuedExecutor
is part of Doug Lea's util.concurrent package -
see Resources.) The remote model
notifies its listeners using RMI callback.
To support visual feedback, RemoteTable sends Task events to registered
Task listeners. The listener's taskStarted() method is called when
a task is scheduled, and taskEnded() is called when it completes. The
demo clients use these events to start and stop a little animation
and update the status line.
Execution sequence
Figure 5 shows the cell update process. Execution begins and ends
in the event-dispatch thread on the left. SwingWorker tasks run
in the executor's thread on the right. Execution of the worker's
finished() method is not shown.
Figure 5. RemoteTable execution sequence.
Simplifications
For simplicity, the remote model does not protect against conflicting
edits. Only one editor should be run at a time. (Concurrent edits
can be supported by adding request IDs.)
Another simplifying decision is that the clients and server must
agree beforehand on the column structure of the table. In other
words, the server supplies the row data to the clients, but the
clients must already know the kind of table they're dealing with.
The demo clients use DefaultModelTemplate to predefine the column
names and classes, and to determine which cells may be edited. (In
the demo, the first two columns are not editable.)
The rest of this section describes the class structure and implementation.
Read on or skip ahead to learn about
the SwingWorker version used in these demos. The Downloading
section explains how to download and run this demo.
Implementation
Figure 6 shows a rough diagram of the class structure.
Figure 6. RemoteTable classes.
The remote model implements the RemoteTableModel interface, which
looks something like an AbstractTableModel, except all the methods
can throw exceptions. To jump-start a new client, the remote table
model sends a full update event to the client's listener when the
listener is registered. Remote table model events contain the values
for the updated cells.
RemoteTableModelAdapter adapts any TableModel to a RemoteTableModel.
The demo table model was lifted from The
Java Tutorial and delays were inserted for simulation purposes.
The RemoteTableModelAdapter uses a ReadWriteLock to control access
to the adapted table model. (ReadWriteLock is also part of the util.concurrent package - see Resources.)
The RemoteTable component uses a DefaultRemoteTableModelListener
to receive the callbacks from the remote model. The listener updates
the local model on the event-dispatch thread. Because the remote
model may report that rows have been inserted or deleted, the listener
requires the local model to be a DefaultTableModel, which supports
insertion and deletion.
SwingWorker Revised
The demos use SwingWorker when they need to run a time-consuming
task in the background and then update the UI.
The SwingWorker used in the demos is based on the one presented
in "Using
a Swing Worker Thread," but has been reimplemented
to fix a race condition, add timeout support, and improve the exception
handling.
The new implementation is based on the FutureResult class from
Doug Lea's util.concurrent
package (see Resources).
By relying on FutureResult for most of the work, the SwingWorker
implementation is simple and flexible.
The rest of this section describes the implementation in more detail.
Read on or skip ahead to download the source.
Runnable FutureResult
A FutureResult, as its name suggests, is a holder for the result
of an action. It is designed to be used with a Callable, which is
a runnable action that returns a result:
public interface Callable {
Object call() throws Exception;
}
The new SwingWorker is a Runnable FutureResult. When run, it sets
the result to the value returned by construct(), and then invokes finished()
on the event-dispatch thread. (Note: SwingWorker is an abstract
class; you subclass it to implement construct() and finished().)
Here's the code from SwingWorker's run() method:
Callable function = new Callable() {
public Object call() throws Exception {
return construct();
}
};
Runnable doFinished = new Runnable() {
public void run() {
finished();
}
};
setter(function).run();
SwingUtilities.invokeLater(doFinished);
The first statement converts construct() into a Callable function, and the
second statement converts finished() to doFinished, a Runnable. Then setter(function)
is run, and doFinished
is invoked.
setter(function)
The missing piece is setter(function).
It creates an ironclad Runnable that, when run, calls function and sets the result to the
value returned. Here's the code from FutureResult:
public Runnable setter(final Callable function) {
return new Runnable() {
public void run() {
try {
set(function.call());
}
catch(Throwable ex) {
setException(ex);
}
}
};
}
Note the try-catch armor. If construct() throws anything (Exception, Error,
whatever), it will be caught and recorded.
Don't race: construct first, then start
Call start() to start
the worker thread. That's an important difference between this SwingWorker
and the original.
Originally, the SwingWorker()
constructor started the thread automatically, but this created a
dangerous race between the thread and the subclass constructor:
when the thread is started in SwingWorker(), the subclass constructor
has not yet completed. Instead, construct SwingWorker first and
then call start().
By the way, RemoteTable does not call start(). Rather, the SwingWorker is executed
as a Runnable by the QueuedExecutor.
Timeout support
The new SwingWorker supports a timeout, which is enabled by overriding
getTimeout() to return
a non-zero value. When the timeout expires, the worker thread is
interrupted.
For an example, see the commented-out getTimeout() method and the TimeoutException
handling in DynamicTree.
Timeouts are implemented using TimedCallable, which uses FutureResult's
timedGet() method.
Improved exception handling
Anything thrown by construct()
is caught and recorded. Barring an infinite loop or a deadlock,
this ensures that the SwingWorker will eventually become ready.
That is, it will either hold a valid result, or it will hold an
exception.
The get() method retrieves
the result. It's inherited from FutureResult:
public Object get()
throws InvocationTargetException, InterruptedException
The get() method will
throw InvocationTargetException if construct() threw an Exception. The
actual exception thrown by construct() can be retrieved by calling getTargetException().
The get() method will
throw InterruptedException if the getting thread is interrupted
while waiting for the result - but this rarely happens with a SwingWorker
because the getting thread is usually the event-dispatch thread,
and the result will always be ready before finished() is ever invoked.
More invoke utilities
The SwingWorker implementation is in the jozart.swingutils package. In this
package you will also find InvokeUtils, which provides a few more
invokeXXX() methods. Background threads may
use these methods to get a value, or user input, on the event-dispatch
thread and return the result to the background thread.
Downloading
The source-code files for all the demos, along with class files
and resources, are contained in the zip file: threads3
demos.zip
The threads3_demos.zip file contains:
- jozart/
- dynamictree/ - DynamicTree demo.
- remotetable/ - RemoteTable bean source.
- remotetabledemo/ - RemoteTable demo.
- swingutils/ - SwingWorker.
- EDU/ - util.concurrent package (only referenced
classes).
- java.policy - RMI security policy.
- remotetable.jar - RemoteTable bean (for IDEs).
NOTE: The util.concurrent
classes are from v1.2.3. Only those classes used by the demos are
included.
NOTE: Java 2 is required to run the demos. (The util.concurrent package uses Collections
in a few places.)
To run the demos, first unzip threads3_demos.zip into an empty
folder, being careful to preserve the folder names. Then change
to the folder where you unzipped the files.
Running DynamicTree
To run DynamicTree as an applet:
> appletviewer jozart/dynamictree/DynamicTree.html
To run DynamicTree as an application:
> java jozart.dynamictree.DynamicTree
Running RemoteTableDemo
The remote table server and clients are designed to be run separately
using RMI. But RMI can be tricky to set up, requiring on an RMI
registry, an HTTP server, and a security policy file. Fortunately,
there's another demo with fewer dependencies. I'll explain how to
run it first.
RemoteTableDemo is an editor, a viewer, and a server all rolled
into one.
To run RemoteTableDemo as an applet:
> appletviewer jozart/remotetabledemo/RemoteTableDemo.html
To run RemoteTableDemo as an application:
> java jozart.remotetabledemo.RemoteTableDemo
Running server and clients separately
To run the remote table server and clients separately, perform
the following steps:
1. Make sure the rmiregistry is running:
> start rmiregistry
2. Make sure an http server is running:
> start httpd
3. Change to the demo directory.
> cd threads3
4. Start the server and clients:
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableServer
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableEditor
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableViewer
You'll need to supply your hostname in the codebase setting. You
should also check that java.policy isn't granting something you'd rather
it didn't.
There's more info about RMI in The Java Tutorial (see
Resources).
Conclusion
This article demonstrated two methods for using threads with model-based
components like JTable and JTree. A revised SwingWorker utility
was also presented.
In the introduction, I stated that building thread safe components
was hard. Java provides language-level support for threads and locks,
but that doesn't suddenly make concurrent programming easy. Swing's
single-thread rule shields programmers from the complexity of thread
safety in everyday use, but imposes a penalty when threads are needed.
To make concurrent programming easier, for myself and for future
users of my code, I've used packaged utilities, proven design patterns,
and established convention as much as possible.
In closing, I offer this advice to anyone interested in extending
SwingWorker or developing their own thread utilities: Know thy
memory model.
In addition to controlling access to shared resources, Java's synchronized
statement also ensures the transmission of values between threads.
This important detail is often overlooked by developers. More information
about the Java Memory Model is available in Doug Lea's online supplement
to Concurrent Programming in Java. Links to this and other
resources are listed in the next section.
Resources
- Threads
and Swing - Hans Muller and Kathy Walrath.
- Using
a Swing Worker Thread - Hans Muller and Kathy Walrath.
- Understanding the TreeModel
- Eric Armstrong, Tom Santos, Steve Wilson.
- The JTable Class
is DB-Aware - Philip Milne and Mark Andrews.
- The
Java Tutorial on RMI - Ann Wollrath and Jim Waldo.
- Concurrent Programming
in Java - Doug Lea.
- util.concurrent
package - Doug Lea.
About the author
Joseph Bowbeer is a Java consultant
living in Seattle. His fascination with concurrent programming began
the day his cursor stopped blinking. He thanks Doug Lea, Tom May,
David Holmes, and Joshua Bloch for sharing their knowledge. Any
errors and omissions are his own doing.
|