Threading
As we discussed earlier, Swing relies on the older AWT GUI toolkit for top-level window support and event dispatching. Whenever you run a Swing application, three threads are automatically created. The first one is the main thread, which runs your application’s main method. A second thread, called the toolkit thread, is in charge of capturing the system events, like keyboard key presses or mouse movements. Although this thread is vital, it is only part of AWT implementation and never runs application code. Capture events are sent over to a third thread, the EDT. The EDT is very important because it is in charge of dispatching the events captured by the toolkit thread to the appropriate components and calling the painting methods. It is also the thread on which you interact with Swing. For instance, if you press a key in a JTextField, the EDT dispatches the key press events to the component’s key listener. The component then updates its model and posts a paint request to the event queue. The EDT dequeues the paint request and notifies the component a second time, asking it to repaint itself. In short, everything in AWT and Swing happens on the EDT. Note that if events are received faster
than they can be delivered, the EDT queues them until they can be processed. While easy to understand on the surface, this simple threading model can yield poor performance in Swing applications if the implications of the Swing’s singlethreaded model are not considered. Indeed, performing a long operation on the EDT, such as reading or writing a file, will block the whole UI. No event can then be dispatched and no update of the screen can be performed while the long operation is underway. The result from the user perspective is that the application appears to be hung or, at least, very slow.
Poorly written applications that block the EDT for long periods of time have
contributed to some people thinking that Swing itself is slow. Most Swing
application performance issues are actually perceived performance issues. The
Swing components dispatch their work quite quickly. However, when the application blocks the EDT, it freezes the user interface and the user thinks the application runs slowly. Freezing happens, for instance, when you have long computations or I/O accesses running in a method executed by the EDT.
The following example, available on the book’s Web site, exhibits such behavior. Run the application and click the Freeze button. It should remain pressed for a few seconds. Whenever the user clicks on a button, the actionPerformed() method of the button’s ActionListener is called. Since this action is triggered by an event, actionPerformed() is invoked on the EDT. In this particular case, the code pauses the current thread for 4 seconds, emulating a long operation that blocks Swing’s ability to dispatch events and repaint the GUI. The following example shows that failure to understand and master Swing’s threading can lead to applications that perform poorly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
public class FreezeEDT extends JFrame implements ActionListener {
public FreezeEDT() {
super("Freeze");
JButton freezer = new JButton("Freeze");
freezer.addActionListener(this);
add(freezer);
pack();
}
public void actionPerformed(ActionEvent e) {
// Simulates a long running operation.
// For instance: reading a large file,
// performing network operations, etc.
try {
Thread.sleep(4000);
} catch (InterruptedException e) {}
}
public static void main(String... args) {
FreezeEDT edt = new FreezeEDT();
edt.setVisible(true);
}
} |
Threading Model
Swing’s threading model is based on a single rule: The EDT is responsible for
executing any method that modifies a component’s state. This includes any component’s constructor. According to this rule, and despite what you can read in many books and tutorials about Swing, the main() method in the previous code example is invalid and can cause a deadlock. Because the JFrame is a Swing component, and because it instantiates another Swing component, it should be created on the EDT, not on the main thread.
Swing is not a “thread-safe” API. It should be invoked only on the EDT. The
great minds behind Swing made this choice on purpose to guarantee the order
and predictability of events. A single-threaded API is also much simpler to
understand and debug than a multithreaded one. Incidentally, Swing is not the only single-threaded graphical toolkit: SWT, QT, and .NET WinForms provide a similar threading model. Now that you know that you must avoid performing any lengthy operations on the EDT, you need to find a solution to this common problem. The first answer that springs to mind is to use another thread, as in the following code example. In this actionPerformed() method, a new thread is spawned to read a large file (of, say, several megabytes) and add the results in a JTextArea:
1 2 3 4 5 6 7 8 9 10 11
|
public void actionPerformed( ActionEvent e ) {
new Thread(new Runnable() {
public void run() {
String text = readHugeFile();
// Bad code alert: modifying textArea on this thread
// violates the EDT rule
textArea.setText(text);
}
}).start();
} |
At first, this code seems to be the solution to your problem, as it does not block the EDT. Unfortunately, it violates Swing’s single-thread rule: it doesn’t modify the text component’s state on the EDT. Doing so will not necessarily cause any trouble during your tests, but a deadlock can appear anytime, and more often than not, it will happen when one of your customers is using the application. Tracking down and fixing such a bug is very difficult and time consuming, so it is highly recommended to always follow Swing’s single-threading rule.
Invoke Later
But don’t fret! Swing offers three very useful methods to deal with the EDT in
the class javax.swing.SwingUtilities. The first of these methods is called
invokeLater(), and it can be used to post a new task on the EDT. Here is how
you can rewrite the previous example to be both nonblocking and correct:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public void actionPerformed( ActionEvent e ) {
new Thread(new Runnable() {
public void run() {
final String text = readHugeFile();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
textArea.setText(text);
}
});
}
}).start();
} |
In the new version of the code, the application posts a Runnable task that updates the content of the text area on the EDT. The invokeLater() implementation takes care of creating and queuing a special event that contains the Runnable. This event is processed on the EDT in the order it was received, just like any other event. When its time comes, it is dispatched by running the Runnable’s run() method. Using invokeLater() is as simple as passing a Runnable instance whose sole method, run(), contains the code you wish to execute on the EDT. So what exactly happens in this code? First, the user clicks on a button and the EDT invokes actionPerformed(). Then, the application creates and starts a new thread, which reads the content of a file and stores it in a String. Finally, a new task, the Runnable instance, is created and placed in the queue of the EDT thanks to invokeLater(). Now that you know how to force a block of code to be invoked on the EDT, it is easy to fix the main() method of the first example:
1 2 3 4 5 6 7 8 9
|
public static void main( String... args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
FreezeEDT edt = new FreezeEDT();
edt.setVisible(true);
}
});
} |
Is This the EDT?
The second SwingUtilities method that makes it easier to work with Swing’s
threading model is called isEventDispatchThread(). When invoked, this
method returns true if the calling code is currently being executed on the EDT, false otherwise. You can therefore create methods that can be called from the EDT and any other thread and still obey the rule, as shown in the following example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
private void incrementLabel() {
tickCounter++;
Runnable code = new Runnable() {
public void run() {
counter.setText(String.valueOf(tickCounter));
}
};
if (SwingUtilities.isEventDispatchThread()) {
code.run();
}
else {
SwingUtilities.invokeLater(code);
}
} |
This method uses an integer, tickCounter, to change the text of a JLabel called counter. When incrementLabel() is called from the EDT, the code executes directly. Otherwise, invokeLater() is used to schedule the task for the EDT. A full working version of this example can be found on the book’s Web site under the name SwingThreading. The third and last SwingUtilities method related to threading, invokeAndWait(), is also the least commonly used (which is probably a good thing, as we will see). Its behavior is similar to invokeLater() in that it allows you to post a Runnable task to be executed on the EDT. The difference is that invokeAndWait() blocks the current thread and waits until the EDT is done executing the task. Let us imagine an intelligent application that can detect when the time it has spent reading a file has exceeded some threshold. This application reads a file in a separate thread and, after 10 seconds of work, asks the user whether he would
like to continue or cancel the operation. To implement such a feature, you would normally initialize a lock to stop the reader thread and then display a dialog box on the EDT. Writing such a piece of code is possible but dangerous because you can easily introduce a deadlock. The following example shows how you can use invokeAndWait() to do the job safely. The complete, executable version of this example is called SwingThreadingWait and can be found on the book’s Web site.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
try {
// Holds the answer to the dialog box
final int[] answer = new int[1];
// Pauses the current thread until the dialog box
// is dismissed
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
answer[0] = JOptionPane.showConfirmDialog(null,"Abort long operation?", "Abort?", JOptionPane.YES_NO_OPTION);
}
});
if (answer[0] == JOptionPane.YES_OPTION) {
return;
}
} catch (InterruptedException ie) {
} catch (InvocationTargetException ite) {
} |
Swing developers should be aware, however, that there is deadlock potential in invokeAndWait(), as there is in any code that creates a thread interdependency. If the calling code holds some lock (explicitly or implicitly) that the code called through invokeAndWait() requires, then the EDT code will wait for the non-EDT code to release the lock, which cannot happen because the non-EDT code is waiting for the EDT code to complete, and the application will hang. In general, invokeAndWait() may appear simpler to use than invokeLater(), because it executes a Runnable task synchronously, and it would seem as though you don’t have to worry about more than one thread executing your code at the same time. But it is risky to use if you are not absolutely sure of the threading and locking dependencies that you are creating, so it should be used only in very clearly risk-free situations.
Besides the three utility methods just covered, every Swing component offers two useful methods that can be called from any thread: repaint() and revalidate(). The latter forces a component to lay out its children, and the former simply refreshes the display. These two methods do their work on the EDT, no matter what thread you invoke them on. The repaint() method is widely used throughout the Swing API to synchronize components’ properties and the screen. For instance, when you change the foreground color of a button by calling JButton.setForeground(Color), Swing stores the new color and calls repaint() to automatically show the new value of the color property. Calling repaint() triggers the execution of several other methods on the EDT, including paint() and paintComponent(). If you place the following component in a JFrame and spawn a new thread called repaint() on the component, you will always see the message “true” in the console output. The complete running example, called SafeRepaint, can be found on this book’s Web site.
1 2 3 4 5 6 7 8 9 10
|
public class SafeComponent extends JLabel {
public SafeComponent() {
super("Safe Repaint");
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
System.out.println(SwingUtilities.isEventDispatchThread());
}
} |
Partager