Asynchronous Operations in Swing
Originally published: 2010-01-18
Last updated: 2020-09-12
“Why does my program lock up?” That's a common question from new Swing programmers, and the usual answer is that they're doing some time-consuming operation in an event handler. They might be waiting for data from a socket connection, or loading a large file from disk. Because Swing uses a single thread to process events, this long-running operation freezes the UI: the user can't type or press a button, and none of the GUI components can repaint themselves.
This article looks at how to manage threads in Swing, keeping these long-running operations from affecting the UI. There's more to success than simply spinning up background threads. You need to get the results of these operations to display in the GUI, control the sequencing of not-quite-independent operations, and provide feedback to the user. We'll look at each of these goals, using an example program that manages files on Amazon's Simple Storage Service (S3).
It's All About Events
Graphical User Interfaces are truly a different paradigm from tradional programs, both from the user's perspective and the programmer's. Traditional programs are linear: you start, process some data, and then you're done. It doesn't matter whether your program is a transaction processing system written in COBOL in the 1960s, or a servlet written in Java last month; they both follow a linear execution path.
Graphical user interfaces, by comparison, receive a steady stream of
independent but related events representing the user's actions:
mouse moves, clicks, and keystrokes. Most of the time, these events are
handled without any program code at all: if the user is typing in a text
field, the events for her keystrokes are captured by the field itself; the
program can later call getText()
to read the field's
contents. Other events are translated into high-level “actions”:
when the user selects a menu item, Swing invokes a “listener”
that responds to that action. These listeners seem to follow the linear
paradigm: they start, do some work, then finish.
But there's an important caveat: in Swing, all action listeners are invoked on a single thread, which happens to be the same thread that Swing uses to dispatch low-level GUI events. If the listener takes a long time to run, those events won't be delivered to their components, and the GUI will “freeze up” until the action listener completes. And then all of the delayed events will be delivered, some of which may invoke other long-running listeners. Faced with a program that's unresponsive for several seconds, most users give up. Those that don't file bug reports.
As I said before, the events are independent but related: they represent an ongoing chain of user actions. Clearly, we need some rules for managing these interactions.
Rules for Interacting with the Event Thread
Like all rules, there are times when these should be broken. That said, here are the rules that I live by when writing Swing programs:
Rule #1: only access Swing components from the event thread.
This means every access. Yes, there are some GUI objects that are explicitly documented as being safe to call from another thread. And the real rule is that rendered components must be updated from the event thread. But it's easier just to follow the general rule and never touch any graphical object from a background thread.
Rule #2: do not run time-consuming operations on the event thread.
I'll keep repeating this: Swing uses a single thread for all GUI events. If your event handler is uploading a multi-megabyte file across the net, those events will be delayed until you're done. There is a balancing act involved with this rule: some operations (such as getting the size of a file) are so fast that they won't interrupt the user. However, that's not a reason to be cavalier, because some day your user will be getting the size of a file that resides on a fileserver that has an intermittent network connection.
Rule #3: do not access synchronized objects from the event thread.
This is really just a corollary to rule #2, and again, doesn't apply in all
cases — after all, many Swing components use Vector
,
which is a synchronized object. The real concern is a synchronized method
that will block for a long time when you call it, such as
LinkedBlockingQueue.put()
.
Rule #4: no shared mutable state.
To me this is the most important rule of all, and is the main point of this article. The earlier rules are driven by the goal of keeping your users happy; this rule is driven by the goal of keeping you, the programmer, sane.
When you have shared mutable state, such as a TableModel
that is updated from the event thread as well as a background thread, you make
your program non-deterministic. If someone finds a bug, it may be caused by
the timing of independent threads, and you may never be able to reproduce it.
Mainstream software developers are slowly accepting the shared-nothing
“actor” model of computation; good GUI developers have practiced
it all along.
Inter-Thread Communication
So, in a shared-nothing world, how do you communicate between threads? The
answer is simple: SwingUtilities.invokeLater(). When your long-running operation is done,
it creates a Runnable
that updates
the GUI with its results. Here's how the interaction might unfold for a
file download:
- The user selects a menu item to request the download.
- The action listener for this menu item constructs a
Runnable
containing all of the information needed to perform the download, such as the filename and destination directory. My preference is to define concrete classes for each operation, rather than attempting to use anonymous inner classes. Regardless, the instance is passed to a threadpool for execution. - At some later time (perhaps immediately, perhaps after it's processed all outstanding work), the threadpool starts running the download operation. While running, this operation is not permitted to ask for additional information from the user, because that would block the background thread. Nor is it permitted to use data held by a GUI component, because they are only to be accessed from the event thread.
- When the download has completed, the operation puts together all the
information that it wants to report back to the user (for example, the
size of the file and the time it took to download), and packages it in
another
Runnable
. That object is then passed toSwingUtilities.invokeLater()
, which queues it for execution on the event thread. - This second
Runnable
updates the GUI; perhaps it simply displays a dialog box with the download summary. In a well-designed two-stage operation, this second stage does't do a lot of work, so it won't affect the GUI.
The main thing to remember is that, once a Runnable
is
passed to another thread, the original thread never updates the data it holds.
What happens on the other thread stays on the other thread.
AsynchronousOperation
So how should you execute tasks on their own thread? One approach is to
simply call new Thread()
every time you want to run
something, passing it the Runnable
that encapsulates
your task. This has a couple of drawbacks. First, it has no inherent limits:
you can keep creating new threads until you run out of virtual memory (although
you'll probably can swamp the thread scheduler long before that happens).
A second, less well-known reason is that new threads inherit the priority of the threads that created them, and the event dispatch thread runs at a higher than normal priority (see this Sun technote). There's a good reason for running the event thread at a high priority: it improves UI responsiveness. But if background threads run at the same priority, that benefit is lost. In the worst case, with a lot of background threads running, the event thread may be starved for CPU leading to an unresponsive GUI.
As an alternative to creating your own threads, Sun provides the
SwingWorker
class. Prior to JDK 1.6
this class was an unsupported download; with the 1.6 release it became a
full-fledged part of the javax.swing
package. However,
I've never particularly liked SwingWorker
: early versions
created new threads for each invocation, and the current version still manages
its own threadpool. In my view, it's trying to do too much, and hides much of its
operation from the programmer.
Instead, I use
AsynchronousOperation
,
part of my
SwingLib open source library. This class relies on the application managing its own
threadpool (or spinning up ad hoc threads), and the implementation of its
run()
method is very simple:
public final void run() { try { final T result = performOperation(); SwingUtilities.invokeLater(new Runnable() { public void run() { onComplete(); onSuccess(result); } }); } catch (final Throwable e) { SwingUtilities.invokeLater(new Runnable() { public void run() { onComplete(); onFailure(e); } }); } }
This class follows the Template Method pattern: it manages the sequence of operations, while you provide the behavior by overriding specific methods:
-
performOperation()
does the actual work, and runs on the background thread. It can either return a result or throw. -
onComplete()
is called on the event thread regardless of whetherperformOperation()
was successful. It is intended to do any common GUI cleanup tasks, such as closing a dialog. It is not intended to clean up afterperformOperation()
. In may cases, you'll just use the default implementation, which does nothing. -
onSuccess()
is called ifperformOperation()
succeeeds, to update the GUI. -
onFailure()
is called ifperformOperation()
fails, to report that failure to the user. Often, all it does is display an error dialog.
The design of the class — taking the return value from one method and passing it to another — encourages shared-nothing programming. You can, of course, define member variables that are accessed by all methods, but this is discouraged (you will, however, often create member variables to be used in one phase or the other, just not both).
Here's an example implementation that loads a list of files from Amazon S3:
@Override protected FileListTableModel performOperation() throws Exception { _logger.debug("starting refresh"); List<Map<String,String>> data = getBucket().listObjectsWithMetadata(); SortedSet<S3File> result = new TreeSet<S3File>(); for (Map<String,String> entry : data) { String key = entry.get(S3Constants.BUCKETLIST_KEY); String size = entry.get(S3Constants.BUCKETLIST_SIZE); String lastMod = entry.get(S3Constants.BUCKETLIST_LASTMOD); result.add(new S3File(key, size, lastMod)); } _logger.debug("request complete: " + result.size() + " files"); return new FileListTableModel(result); } @Override protected void onSuccess(FileListTableModel result) { getConcierge().getMainFrame().resetList(result); }
As I noted above, the onSuccess()
method is quite short:
it takes the result and hands it off to the controller for the table. This
reflects my design philosophy about partitioning knowledge within an
application: the operation knows how to retrieve data, but it has no
reason to know what happens to that data once it's retrieved.
While the implementation of performOperation()
might not
make a lot of sense unless you've used the
S34J library,
there is one thing that I want to point out: it does all of the work.
The web service call happens at the very beginning; the rest of the method
simply processes the returned data, and the result is a fully-initialized
TableModel
. That processing code could have been put in
onComplete()
without breaking any of the rules. But by
running in on the background thread, I guarantee that the user won't notice
any delays.
You may be wondering why this example omits the onComplete()
and onFailure()
methods. These will be shown later.
Providing Feedback
Moving long-running operations to a background thread is only part of creating a user-friendly application. Providing feedback to the user is often more important. The refresh operation above generally takes a few hundred milliseconds to execute. That's fast, but still something that a user would notice: “I've clicked the menu, nothing's happening!” Uploading or downloading files, however, is guaranteed to be noticed.
The Busy Cursor
One of the simplest ways to indicate that the application is busy is to change its cursor to a “busy cursor”:
The busy cursor is one of the “predefined” cursors provided by
the Cursor
class. And all Swing
components have a setCursor()
method that changes the
cursor displayed when the mouse is over that specific component (see the
CursorDemo example).
Changing the cursor over a single component can be useful: a complex application might update one set of components while the user is still able to interact with others; you could show the busy cursor over the first set and the normal cursor over the second.
Most of the time, however, you'll set the wait cursor on the application's root
pane to indicate overall status. But this introduces its own complexity: what if
there are multiple operations happening at once? If you simply switch to the busy
cursor at the start of an operation, and back to the normal cursor when it's done,
overlapping operations will leave you with the wrong cursor. Instead, you have to
create a stack of cursors (these methods methods are copied from
CursorManager
,
another part of the SwingLib library):
public void pushCursor(JComponent comp, Cursor newCursor) { LinkedList<Cursor> stack = getCursorStackFor(comp); stack.add(comp.getCursor()); comp.setCursor(newCursor); } public void popCursor(JComponent comp) { LinkedList<Cursor> stack = getCursorStackFor(comp); if (stack.size() == 0) return; comp.setCursor(stack.removeLast()); }
Incorporating a Busy Cursor into AsynchronousOperation
AsynchronousOperation
is a two-phase operation: it
starts running on a background thread, then completes on the event thread.
But to incorporate a busy cursor you need a three-phase operation, which
starts running on the event thread and then puts itself on the background
thread. Since I don't want AsynchronousOperation
to
manage its own threads, this calls for an application-specific subclass:
public abstract class AbstractS3Op<T> extends AsynchronousOperation<T> { private Concierge _concierge; private String _description; // member variables specific to the application protected AbstractS3Op(Concierge concierge, String description) { _concierge = concierge; _description = description; } public void start() { _concierge.getMainFrame().setBusyState(true); _concierge.execute(this); } @Override protected void onComplete() { _concierge.getMainFrame().setBusyState(false); }
The Concierge
will be described in another article;
it's my way to avoid singletons yet still provide easy access to shared
resources. One of these resources is the background thread pool, and the
last step of the start()
method is to pass the operation
object to that pool.
The busy-cursor feedback is managed by the main window controller; this
application just has a single window, so it makes sense to put the logic
there. In an application with multiple independent windows, I would have
a central CursorManager
, accessed via the Concierge.
We want to change the cursor back to its normal state regardless of whether
the operation succeeded or failed. This is the use case for
onComplete()
. As you saw above, it is executed on the event
prior to both onSuccess()
and onFailure()
.
But be careful: because it's executed before these methods, it must
not throw or they'll never be executed.
ProgressMonitor
While a busy cursor is useful, it doesn't provide a lot of feedback to the
user. For a long-running operation, such as a file upload, it's better to
display a progress dialog. If you update that dialog as the operation is
running, better still. The JDK provides
ProgressMonitor
to fill this role, but it has a few nasty quirks:
- It has a fixed range, and will close if you call
setProgress()
with a value larger than the configured maximum. That also means that it isn't very useful in situations where you don't know the maximum value, such as retrieving a list of files from a server. Yes, you can update the range during operation, but that doesn't present a very nice user experience (think of Windows Explorer doing a file copy: it will say that you have 30 seconds left, then 7 minutes, then 1 minute, and so on). - It decides when to display itself, based on program-configurable parameters along with its estimation of how much time the operation will take. True, this is a better user experience than flashing the dialog on-screen for a fraction of a second. However, as a programmer I'd prefer to control my program's behavior.
- It must be updated from the event dispatch thread. Yes, just like any other
Swing component, but
ProgressMonitor
's sole reason to exist is to show the progress of a background operation! There was a bug filed about this years ago (actually about another JDK class that uses a progress monitor), but it was closed with no plan to fix.
As a result, I've implemented my own version (part of SwingLib, of course). And
using it, the start()
and onComplete()
methods of AbstractS3Op
actually look like this:
public void start() { setBusyState(true); _concierge.execute(this); } protected void onComplete() { setBusyState(false); } private void setBusyState(boolean isBusy) { _concierge.getMainFrame().setBusyState(isBusy); if (isBusy) { _progressMonitor = new ProgressMonitor( _concierge.getDialogOwner(), "S3 Operation in Progress", _description, ProgressMonitor.Options.MODAL, ProgressMonitor.Options.CENTER, ProgressMonitor.Options.SHOW_STATUS); _progressMonitor.show(); } else if (_progressMonitor != null) { _progressMonitor.dispose(); _progressMonitor = null; } } protected void updateProgressMonitor(String message) { _progressMonitor.setStatus(message); }
Unlike the JDK's progress dialog, which decides when it should appear based on
elapsed time, my operations display the progress dialog immediately. The
updateProgressMonitor()
method is meant to be called while
the operation is running, to give the user constant feedback. Here's an
example from the delete operation, which displays each file as it's deleted:
@Override protected Object performOperation() throws Exception { _logger.debug("starting request"); for (S3File file : _files) { _logger.debug(file); String key = file.getKey(); S3Object obj = createS3Object(key); obj.delete(); updateProgressMonitor(file.toString()); } _logger.debug("request complete"); return null; }
And, just in case you're wondering, the progress monitor internally uses
SwingUtilities.invokeLater()
to update the actual dialog:
public void setStatus(final String message) { SwingUtilities.invokeLater(new Runnable() { public void run() { _fStatus.setText(message); } }); }
Interactive Updates
Another way to provide feedback from long-running operations is to update the UI while the operation is running. For example, as you upload files via S3Util, not only does the progress dialog tell you what's happening, but you can see rows being added to the main list:
Within the loop that uploads files, there's a call to this method:
private void reportFileUploaded(final S3File file) { SwingUtilities.invokeLater(new Runnable() { public void run() { getConcierge().getMainFrame().addFile(file); } }); }
With the uploads being reported as they happen, there's no reason for the
operation to call onSuccess()
. But that's OK, the default
implementation in AsynchronousOperation
is empty.
From a user experience perspective, such in-your-face updates can be annoying: I'm sure we've all visited websites that display a popup while in the middle of a long operation. And if you have a lot of updates, you'll reduce the overall throughput of your program (at least on a single-core processor), since the UI will be constantly repainting. So put yourself in the position of the user before adding interactive updates to your code, and avoid using them when the user doesn't receive some benefit (in the case of file uploads, it can be useful: if you see that files are going into the wrong directory you could hit the “Cancel” button — except that S3Util 1.0 doesn't have one).
Exception Handling
The default implementation of AsynchronousOperation.onFailure()
wraps the passed exception in RuntimeException
and rethrows it;
the Swing event thread logs all uncaught exceptions but otherwise ignores them.
This is almost guaranteed to be the wrong thing to do, but the right thing
depends on the application. In the case of S3Util, a tool meant for programmers and
system administrators, AbstractS3Op
provides a simple implementation:
@Override protected void onFailure(Throwable ex) { LogFactory.getLog(this.getClass()).error("request failed", ex); JOptionPane.showMessageDialog( _concierge.getDialogOwner(), "Unable to process this request: " + ex.getMessage() + "<br>See log for more information", "Unable to Execute Operation", JOptionPane.ERROR_MESSAGE); }
In an application targeted to end users, you'll want to do more than this. For example, in S3Util a request might fail because the user entered an invalid access key. This information is available in the exception and I could use this fact to display an appropriate message, along with a button that opens the Preferences dialog so that the user could remedy the situation.
Better still is to prevent onFailure()
from being invoked
in the first place. As with a normal Java application, if you can correct an
exception when it is thrown, you should do so. The reason is that
you lose context as you move away from the code that caused the exception;
in a Swing app, you're no longer even on the same thread. So if you can
correct the situation without user input, do so and try the operation
again. Just be careful that you don't create a retry loop that will never
end.
How Many Things Do You Want to Run Simultaneously?
When you execute long-running operations on a threadpool, you'll ask yourself “how many threads do I need?” As multi-core processors become more common, it's tempting to say that the pool should have at least as many threads as you have processors. Or more, if your operations tend to be IO-bound.
That answer, however, ignores the fact that humans aren't very good at multi-tasking, and running too many simultaneous operations can confuse your users. S3Util, for example, has operations (upload, delete, download) that probably shouldn't happen at the same time — why would you ever delete files in the middle of an upload? As a result, it has a single thread in its threadpool, and all operations display modal dialogs.
That said, a lot of applications have CPU-intensive “set and forget” operations such as analyzing data, several of which can be running while the user is interacting with another part of the program. For such an application, I'd think of using two threadpools, one with a single thread for interactive operations, one with many threads for true background operations.
Not Everything Needs Its Own Thread
AsynchronousOperation
does impose costs of its own:
there's a delay before it starts running, a delay as its results are posted
back to the event thread, and then another delay as the component repaints.
It's possible that these delays will take longer than just executing the
operation inline. For example,
TableRowSorter
, a Swing
utility class for enabling sortable columns in a JTable
,
does all of its work on the event thread. For most usable tables (those
with at most a few thousand rows), this happens so fast that the user will
never notice. For larger tables, there will be a delay, but if the user is
working with such large tables they'll expect that delay.
A useful rule-of-thumb is that any operation that relies on resources external to the JVM — particularly resources accessed over a network — should be performed on its own thread, while it's OK to process in-memory data on the event thread. There is a gray area in the middle: numerical analysis, for example, may use in-memory data yet still take significant time to excute.
Ultimately, the choice of whether to use background threads requires thought and experimentation. And always consider whether your development machine is faster or slower than the average user's machine. What you think is acceptable may be horrendously slow to someone without a leading-edge machine (I know of one company that intentionally gave its developers underpowered machines — but then switched from desktop applications to web-apps, where the end-user's machine didn't matter).
What About SwingUtilities.invokeAndWait()?
Throughout this article I've used SwingUtilities.invokeLater()
to move data from a background thread to the event thread. But there's also
an invokeAndWait()
method. Should you ever use it, and if not,
why not?
My answer is “almost never,” and the rationale is that it breaks
rule #3. The call to invokeAndWait()
will block until
its Runnable
finishes, which means that the background
thread won't be able to do anything else. If you're concerned that you need to
order the operations on the event thread, don't be: Runnable
s
passed to invokeLater()
are invoked in the order they were
submitted.
There are some cases where you'll want to chain operations together: run one
operation on the background thread, update the GUI, and only then start the
next operation. Most of the time, this need is also an illusion: sequenced
operations usually require user interaction. In the cases where you truly
need to sequence operations, it's far better to implement that sequencing in
the onSuccess()
method.
In my programs, there's only one place where I call
SwingUtilities.invokeAndWait()
: application startup. And even
here, I think my use is driven more by dogma than reality: I don't want to
risk the main application thread ending (and the program exiting) before the
event thread takes over. So my Swing programs' main()
methods
all look like this:
public static void main(String[] argv) throws Exception { final Concierge concierge = new Concierge(new ConfigBean()); SwingUtilities.invokeAndWait(new Runnable() { public void run() { new MainFrameController(concierge).buildAndShow(); // any additional GUI startup tasks } }); }
A Tip to Improve Responsiveness: Do Work Only When It's Necessary
If you browse the S3Util source code (linked below), you'll see that I create dialog controller objects when the program starts, but don't construct the actual dialogs until the first time they're displayed. This “lazy instantiation” significantly improves application startup time, and does not impact interactiveness — creating a single dialog in an ad hoc manner doesn't take a lot of time.
It's the little things like this that lead to an application's “feel.” Users notice short delays subliminally. They'll never be able to tell you what needs to be fixed, just that it “runs slowly.” But if you pay attention to how much work happens on the event thread, and try to minimize it, performance complaints should be rare. And that's the real goal.
For More Information
The S3Util source code is available on GitHub.
It uses the swinglib library, which contains
AsynchronousOperation
along with other helper classes that I
developed when I was actively working with Swing. It's available as a JAR from
Maven Central in case you'd like to use it as well, but beware that it is not
actively maintained.
If you want to see just how annoying an unresponsive UI can be, run this) example.
Sun has released many articles on Swing and threading; here are some that I've found useful:
- The Swing Tutorial focuses on the
SwingWorker
class. As I've said, I don't particularly like it, but unlike myAsynchronousOperation
it is a supported part of the JDK. - Although it isn't about concurrent programming, I also think the timer tutorial is useful from the perspective of sequencing operations.
- The Last Word in Swing Threads describes techniques to link Swing component models into remote operations.
Copyright © Keith D Gregory, all rights reserved