Java's Swing API is not thread safe in general, but it can be made more so with just a few small additions.
Swing is Java's pluggable look-and-feel user interface toolkit. Unlike many other Java packages and classes, Swing is not multithreaded. Once the user interface is created and painted to the screen, all updates to it must occur in the Swing event dispatch thread. The event dispatch thread is automatically started when the application or applet starts up, and all repaint and listener (user event handling) code is executed in this thread.
In this article I show that Swing's lack of thread safety [1] poses problems when designing supervision applications, since these applications require a separate thread to handle communications with remote machines. I then discuss two capabilities Swing provides to overcome its single thread limitations, and finally present a multithreaded supervision application that illustrates how to overcome these limitations.
User Events in Swing
Swing uses a delegation event model. In this model listener objects are added to user interface components to listen for events on those components. When an event occurs, Swing invokes the appropriate method of each listener in the event dispatch thread.
Figure 1 shows a simple Java application containing Swing JButton and JLabel components. The label displays a count of the number of times the button is clicked. Figure 2 contains a partial listing for this application. The main thread contructs an instance of the ClickCount application class and then lays out and displays the application window:
// Runs in the main thread static public void main(String args[]) { // instantiate application class ClickCount click = new ClickCount(); click.pack(); // layout window and components click.show(); // make visible // from this point on all UI // update must occur in the event thread }The call to show paints the interface on the screen. From this point on, all updates to the interface must occur in the event dispatch thread. When the user clicks on the button, the actionPerformed method is called to update the interface:
// The actionListener runs in the event thread class CountClicks implements ActionListener { public void actionPerformed(ActionEvent e) { label.setText("Count is : " + ++count); // update label ok } }Since actionPerformed runs in the event thread this is okay.
What About Non-User Events?
There are, however, a large number of supervision applications which need to update the UI in response to non-user generated events. Factory automation, network management, and real-time quote display are examples of such applications. Their client UIs display data received from a remote process or processes via sockets or some other messaging mechanism. In all these cases input event processing generally consists of the following minimum sequence of steps:
1. read data
2. process data
3. update the UIThe requirement to update the UI from the event dispatch thread complicates the efficient scheduling of data reads, user event handling, and UI repainting, since a blocking read of a socket in the event dispatch thread will freeze the UI. Conversely, a separate communications thread cannot update the user interface directly since this will break Swing's single-threaded requirements.
A Potential Solution Timers
Swing provides timer facilities for the periodic execution of a task in the event dispatch thread. To use a timer you pass two arguments to the Timer constructor the task, which is an implementation of the ActionListener interface; and the delay time between invocations:
//Class to execute a periodic task class MyPeriodicTask implements ActionListener { public void actionPerformed(ActionEvent e) { // .. do something } } // Create a timer object with a 1000 // millisecond delay to execute an // instance of MyPeriodicTask. Timer timer = new Timer(1000, new MyPeriodicTask()); timer.start(); // start up the timerTimers offer a potential solution to the supervision application problem, since a periodic task could test for data on a socket, read and process any available data, and update the user interface. However, I believe timers are an inappropriate solution for the following reasons:
1. All reading, data processing, and UI update work occurs in the event dispatch thread. If the reading and processing are time-consuming processes then the interface refreshing and user event handling will be sluggish.
2. The lack of correlation between timer firing and the work to be done will either cause unnecessary delays in processing socket data (timer firing too infrequently) or unnecessary sluggishness in the UI (timer firing too often). This is a general problem with the use of polling to detect asynchronous events. For this reason timers are inappropriate for tasks other than animation, clock update, or others that are periodic by nature.
A Better Solution Execution Scheduling
The javax.swing.SwingUtilities class provides two static methods that allow code to be scheduled for execution in the event dispatch thread. These methods are static void invokeAndWait(Runnable task) and static void invokeLater(Runnable task). The Runnable interface (which the scheduled task must implement) contains the single method void run().
invokeAndWait schedules its argument for synchronous execution on the event dispatch thread and returns only when the run method returns. invokeLater simply adds its task to a list of pending tasks on the event dispatch queue and returns immediately. Since invokeLater does not block, it is faster. It is also easier to use, as the following code fragments illustrate:
SwingUtilities.invokeLater ( new Runnable() { public void run() { // ... update the UI // (not shown) } } ); try { SwingUtilities.invokeAndWait ( new Runnable() { public void run() { // ... update the UI // (not shown) } } ); } catch (InterruptedException e1) { // this thread was interrupted // while waiting for run() // to terminate } catch (java.lang.reflect. InvocationTargetException e2) { // an uncaught exception was thrown // during run's execution }invokeAndWait must be enclosed in a try block since it can throw two exceptions. If the current thread is interrupted while waiting for run to terminate, invokeAndWait throws an InterruptedException. If an uncaught exception is thrown during the execution of run, invokeAndWait wraps it in an InvocationTargetException and rethrows it.
Note that in these examples the arguments to invokeLater and invokeAndWait are instances of anonymous classes, which implement the Runnable interface. Anonymous classes can be defined at their point of use and make for more compact code; however, I will show that they are not suitable for all occasions.
A Multithreaded Swing Client
The remainder of the article presents a multithreaded Swing client that monitors the status of a simulated remote process consisting of multiple subsystems. Figure 3 shows the running client. The client receives periodic status messages from various subsystems, which it displays to the user in a scrolled list. When a subsystem fails, the client asks the user to terminate that subsystem. Figure 4 shows the subsystem failure dialog. The simulator is not presented here but source for the entire application is available at www.cuj.com/java/code.htm.
Building the User Interface
Like any Swing application, the client builds and realizes its user interface in the main application thread. Figure 5 shows user interface construction code from SwingClient. The main method creates an instance of SwingClient, whose contructor invokes the buildUI method to construct the user interface. Once the main frame is packed and displayed, all further updates to the user interface must occur in the event dispatch thread.
Socket Communications
The client uses a separate thread to manage communications with the simulator. Figure 6 shows the Comms constructor with error checking removed. The constructor opens a socket to TCP port 4444 on the local machine, then wraps the socket's input and output streams in a BufferedReader and PrintWriter respectively. A BufferReader provides efficient buffering of data from the socket's input stream, while PrintWriter provides convenient methods for writing formatted data to the socket.
Handling Input Data
Figure 7 shows the run method of the Comms thread. When the blocking read returns with a new message from the simulator, run creates a new MessageTask with the message as argument. This message task is then added to the Swing event dispatch queue with invokeLater. Note that I use the named MessageTask class rather than an anonymous class as in the earlier example. This allows me to pass the actual message received as an argument to the constructor. Figure 8 contains the code for MessageTask. The constructor stores the message argument, which the run method adds to the status list.
If the received message is a FAIL message the subsystem name is extracted and stored in the ConfirmTerminateTask. run then passes the ConfirmTerminateTask instance to invokeAndWait. When invokeAndWait returns, the client calls the terminateTask method to see if the user wants to terminate the task. If the user has elected to terminate the failed subsystem, the client sends a Kill command to the simulator and the subsystem name is added to the list of subsystems to filter out of the incoming message list. Here I use invokeAndWait to execute the ConfirmMessageTask rather than invokeLater, since I know the result is available when invokeAndWait returns.
Figure 9 contains the listing for ConfirmTerminateTask. It provides methods to set the subsystem name and retrieve the user's choice.
Conclusion
Although Swing is essentially not multithreaded, it is possible to write multithreaded applications that update the Swing user interface. This can be done using the SwingUtilities invokeLater and invokeAndWait methods. invokeLater is faster and easier to use than invokeAndWait but cannot be used in all situations. As shown here, invokeAndWait must be used when a user interface update task returns a result.
invokeAndWait is also a better option when each task contains state information and memory usage must be kept to a minimum. When invokeAndWait returns, its associated task's run method has finished and the task instance can be reused. Since there is no built-in way of knowing when a task passed to invokeLater has terminated, a new task must be created for each invocation, which increases memory usage. This restriction does not apply to stateless classes, since a single task can be added to the event dispatch thread multiple times.
The use of the invokeLater and invokeAndWait methods is more cumbersome than direct update of the user interface would be from a separate thread. However, Swing's single-threaded architecture offers one major advantage: developers of new Swing classes do not have to concern themselves with rendering their work thread safe. This greatly encourages reuse of existing Swing components and creation of new Swing JavaBeans.
Note
[1] It should be pointed out that some aspects of Swing are thread-safe. It is possible to modify listener lists and invoke repaint and validate methods from any thread.
Peter Meehan is President of LOOX Software Inc. in Los Altos, CA, where he is heavily involved with the LOOX range of dynamic graphics development tools. His interests include user interfaces, graphics and distributed computing, and the application of these technologies to supervision and control applications. He holds a B.A.I. in Computer Science and Electronic Engineering and an M.Sc. in Computer Science from Trinity College Dublin, Ireland. He can be contacted at peter@loox.com.