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.
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.
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.
The Swing single-thread rule: Swing components and models should be created, modified, and queried only from event-dispatching thread.
The Swing event thread can be thought of as a single-threaded
Executor that processes tasks from the event queue.
GuiExecutor is an
Executor that delegates tasks to
SwintUtilities for execution.
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:
A slightly more complicated version of this same scenario, illustrated below, involves the use of a formal data model such as a
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.
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.
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.
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.
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.
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.
After the background
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.
Initiating a long-running, cancellable task with
In Swing, many of the features developed here are provided by the
SwingWorker class, including cancellation, completion notification, and progress indication.
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.
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.
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.
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.
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)