Java Concurrency in Practice Notes: GUI Applications

If you’ve tried to write even a simple GUI application using Swing, you know that GUI applications have their own peculiar threading issues. To maintain safety, certain tasks must run in the Swing event thread. But you cannot execute long-running tasks in the event thread, lest the UI become unresponsive. And Swing data structures are not thread-safe, so you must be careful to confine them to the event thread.

Nearly all GUI toolkits, including Swing and AWT, are implemented as single-threaded subsystems in which all GUI activity is confined to a single thread. If you are not planning to write a totally single-threaded program, there will be activities that run partially in an application thread and partially in the event thread.

Even though the GUI frameworks themeselves are single-threaded systems, your application may not be, and you still need to consider threading issues carefully when writing GUI code.

GUI Applications

Why are GUIs single-threaded?

Single-threaded GUI frameworks are not unique to Java; Qt, NextStep, MacOS Cocoa, X Windows, and many others are also single-threaded. This is not for lack of trying; there have been many attempts to write multi-threaded GUI frameworks, but because of persistent problems with race conditions and deadlock, they all eventually arrived at the single-threaded event queue model in which a dedeicated thread fetches events off a queue and dispatches them to application defined event handlers.

“…The multi-threaded approach works best for people who have been intimately involved in the design of the GUI tookit…, but not for normal smart programmers building apps…”

Single-threaded GUI frameworks achieve thread safety via thread confinement; all GUI objects, including visual components and data models, are accessed exclusively from the event thread.

Sequential event processing

GUI aplications are oriented around processing fine-grained events such as mouse clicks, key presses, or timer expirations.

Because there is only a single thread for processing GUI tasks, they are processed sequentially - one task finishes before the next one begins, and no two tasks overlap.

The downside of sequential task processing is that if one task takes a long time to execute, other tasks must wait until it is finished. If those other tasks are responsible for responding to user input or providing visual feedback, the application will appear to have frozen.

Thread confinement in Swing

The Swing single-thread rule: Swing components and models should be created, modified, and queried only from event-dispatching thread.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SwingUtilities {
private static final ExecutorService exec =
Executors.newSingleThreadExecutor(new SwingThreadFactory());
private static volatile Thread swingThread;
private static class SwingThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
swingThread = new Thread(r);
return swingThread;
}
}
public static boolean isEventDispatchThread() {
return Thread.currentThread() == swingThread;
}
public static void invokeLater(Runnable task) {
exec.execute(task);
}
public static void invokeAndWait(Runnable task)
throws InterruptedException, InvocationTargetException {
Future f = exec.submit(task);
try {
f.get();
} catch (ExecutionException e) {
throw new InvocationTargetException(e);
}
}
}

The Swing event thread can be thought of as a single-threaded Executor that processes tasks from the event queue.

1
2
3
4
5
6
7
8
9
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
public class GuiExecutor extends AbstractExecutorService {
// Singletons have a private constructor and a public factory
private static final GuiExecutor instance = new GuiExecutor();
private GuiExecutor() {
}
public static GuiExecutor instance() {
return instance;
}
public void execute(Runnable r) {
if (SwingUtilities.isEventDispatchThread())
r.run();
else
SwingUtilities.invokeLater(r);
}
public void shutdown() {
throw new UnsupportedOperationException();
}
public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
public boolean isShutdown() {
return false;
}
public boolean isTerminated() {
return false;
}
}

GuiExecutor is an Executor that delegates tasks to SwintUtilities for execution.

Short-running GUI tasks

In a GUI application, events originate in the event thread and bubble up to application-provided listeners, which will probably perform som computation that affects the presentation objects.

  • For simple, short-running tasks, the entire action can stay in the event thread
  • For longer-running tasks, some of the processing should be offloaded to another thread

In the simple case, confining presentation objects to the event thread is completely natural:

1
2
3
4
5
6
7
8
final Random random = new Random();
final JButton button = new JButton("Change color");
//...
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
button.setBackground(new Color(random.nextInt()));
}
})

Simple Case

A slightly more complicated version of this same scenario, illustrated below, involves the use of a formal data model such as a TableModel or TreeModel. Swing split most visual components into two objects, a model and a view. The data to be displayed resides in the model and the rules governing how it is displayed resides in the view.

Separate Case

Long-running GUI tasks

For sophisticated GUI applications, they may execute tasks that may take longer than the user is willing to wait, such as spell checking, background compilation, or fetching remote resources. These tasks must run in another thread so that the GUI remains responsive while they run.

1
2
3
4
5
6
7
8
9
10
11
12
13
private static ExecutorService exec = Executors.newCachedThreadPool();
private void longRunningTask() {
computeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
exec.execute(new Runnable() {
public void run() {
/* Do big computation */
}
});
}
});
}

There is usually some sort of visual feedback when a long-running task completes. But you cannot access presentation objects from the background thread, so on completion the task must submit another task to run in the event thread to update the user interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void longRunningTaskWithFeedback() {
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
button.setEnabled(false);
label.setText("busy");
exec.execute(new Runnable() {
public void run() {
try {
/* Do big computation */
} finally {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
button.setEnabled(true);
label.setText("idle");
}
});
}
}
});
}
});
}

Upon completion, the second subtask queues the third subtask to run again in the event thread, which updates the user interface to reflect that the operation has completed. This sort of “thread hopping” is typical of handling long-running tasks in GUI applications.

Cancellation

Any task that takes long enough to run in another thread probably also takes long enough that the user might want to cancel it. You could implement cancellation directly using thread interruption, but it is much easier to use Future, which was designed to manage cancellable tasks.

1
2
3
4
5
6
7
8
9
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
private Future<?> runningTask = null; // thread-confined
private void taskWithCancellation() {
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (runningTask != null) {
runningTask = exec.submit(new Runnable() {
public void run() {
while (moreWork()) {
if (Thread.currentThread().isInterrupted()) {
cleanUpPartialWork();
break;
}
doSomeWork();
}
}
private boolean moreWork() {
return false;
}
private void cleanUpPartialWork() {
}
private void doSomeWork() {
}
});
}
;
}
});
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
if (runningTask != null)
runningTask.cancel(true);
}
});
}

Because runningTask is confined to the event thread, no synchronization is required when setting or checking it, and the start button listener ensures that only one background task is running at a time.

Progress and completion indication

After the background Callable completes, done is called. By having done trigger a completion task in the event thread, we can construct a BackgroundTask class providing an onCompletion hook that is called in the event thread, such as below.

1
2
3
4
5
6
7
8
9
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
public abstract class BackgroundTask <V> implements Runnable, Future<V> {
private final FutureTask<V> computation = new Computation();
private class Computation extends FutureTask<V> {
public Computation() {
super(new Callable<V>() {
public V call() throws Exception {
return BackgroundTask.this.compute();
}
});
}
protected final void done() {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
V value = null;
Throwable thrown = null;
boolean cancelled = false;
try {
value = get();
} catch (ExecutionException e) {
thrown = e.getCause();
} catch (CancellationException e) {
cancelled = true;
} catch (InterruptedException consumed) {
} finally {
onCompletion(value, thrown, cancelled);
}
};
});
}
}
protected void setProgress(final int current, final int max) {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
onProgress(current, max);
}
});
}
// Called in the background thread
protected abstract V compute() throws Exception;
// Called in the event thread
protected void onCompletion(V result, Throwable exception,
boolean cancelled) {
}
protected void onProgress(int current, int max) {
}
// Other Future methods just forwarded to computation
public boolean cancel(boolean mayInterruptIfRunning) {
return computation.cancel(mayInterruptIfRunning);
}
public V get() throws InterruptedException, ExecutionException {
return computation.get();
}
public V get(long timeout, TimeUnit unit)
throws InterruptedException,
ExecutionException,
TimeoutException {
return computation.get(timeout, unit);
}
public boolean isCancelled() {
return computation.isCancelled();
}
public boolean isDone() {
return computation.isDone();
}
public void run() {
computation.run();
}
}

Initiating a long-running, cancellable task with BackgroundTask

1
2
3
4
5
6
7
8
9
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
private void runInBackground(final Runnable task) {
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
class CancelListener implements ActionListener {
BackgroundTask<?> task;
public void actionPerformed(ActionEvent event) {
if (task != null)
task.cancel(true);
}
}
final CancelListener listener = new CancelListener();
listener.task = new BackgroundTask<Void>() {
public Void compute() {
while (moreWork() && !isCancelled())
doSomeWork();
return null;
}
private boolean moreWork() {
return false;
}
private void doSomeWork() {
}
public void onCompletion(boolean cancelled, String s, Throwable exception) {
cancelButton.removeActionListener(listener);
label.setText("done");
}
};
cancelButton.addActionListener(listener);
exec.execute(task);
}
});
}

SwingWorker

In Swing, many of the features developed here are provided by the SwingWorker class, including cancellation, completion notification, and progress indication.

Shared data models

In simple GUI programs, all the mutable state is held in the representation objects and the only thread besides the event thread is the main thread. In these pograms enforcing the single-thread rule is easy: don’t access the data model or presentation components from the main thread.

In the simplest case, the data in the data model is entered by the user or loaded statically from a file or other data source at application startup, in which case the data is never touched by any thread other than the event thread. But sometimes the presentation model is only a view onto another data source, such as a database, file system, or remote service. In this case, more than one thread is likely to touch the data as ist goes into or out of the application.

Thread-safe data models

As long as responsiveness is not unduly affected by blocking, the problem of multiple threads operating on the data can be addressed with a thread-safe data model. If the data model supports fine-grained concurrency, the event thread and background threads should be able to share it without responsiveness problems.

Split data models

A program that has both a presentation-domain and an application-domain data model is said to have a split-model design. In a split-model design, the presentation model is confined to the event thread and the other model, the shared model, is thread-safe and may be accessed by both the event thread and application threads.

Consider a split-model design when a data model must be shared by more than one thread and implementing a thread-safe data model would be inadvisable because of blocking, consistency, or complexity reasons.

Other forms of single-threaded subsystems

Thread confinement is not restricted to GUIs: it can be used whenever a facility is implemented as a single-threaded subsystem. Sometimes thread confinement is forced on the developer for reasons that have nothing to do with avoiding synchronization or deadlock. e.g. some native libraries require that access to the library, even loading library with System.loadLibrary, be made from the same thread.

Summary

GUI frameworks are nearly always implemented as single-threaded subsystems in which all presentation-related code runs as tasks in an event thread. Because there is only a single event thread, long-running tasks can compromise responsiveness and so should be executed in background threads. Helper classes like SwingWorker or the BackgroundTask class built here, which provide support for cancellation, progress indication, and completion indication, can simplify the development of long-running tasks that have both GUI and non-GUI components.

(To Be Continued)