The Source for Java Technology Collaboration
User: Password:



   

Rethinking Swing Threading Rethinking Swing Threading

by Jonathan Simon
10/24/2003

Improper Swing threading is one of the main causes of sluggish, unresponsive, and unstable Swing applications. There are many reasons for this, from developers not understanding the Swing single threading model, to the difficulty of ensuring proper thread execution. Even when a conscious effort is given to Swing threading, application-threading logic tends to get quite difficult to understand and maintain. This article explains how to use event-driven programming to develop Swing applications, resulting in greatly simplified development, maintenance, and flexibility.

Background

Since we are trying to simplify the threading of Swing applications, let's first take a quick look at how Swing threading works and why it's necessary. The Swing API is designed around the single threading model. This means that Swing components must always be modified and otherwise interacted with using the same thread. There are a number of reasons for the single thread model, including the development cost and complexity of synchronizing Swing -- an already slow API. To facilitate the single threading model, there is a dedicated thread for interacting with Swing components. This thread is known as the Swing thread, the AWT (sometimes pronounced "ought") thread, or the event-dispatch thread. For the rest of this article, I'll refer to is as the Swing thread.

Since the Swing thread is the only thread that should interact with Swing components, it has a lot of responsibilities. All painting and graphics, mouse events, component events, button events, and all other events occur in the Swing thread. Since the Swing thread is already weighted down with work, problems occur when too much other work is executed in the Swing thread. One of the most common places this can occur is in the placement of non-Swing work, like a database lookup, in an event listener method, such as an ActionListener on a JButton. Since the ActionListener's actionPerformed() method automatically gets performed in the Swing thread, the database call is also executed in the Swing thread. This occupies the Swing thread with work, preventing it from performing its other responsibilities -- like painting, responding to mouse movements, processing button events, and application resizing. Users think the application is frozen, but it may not be. Executing the code in the appropriate thread is essential to guarantee that the system is executing properly.

Now that we've taken a look at why it is important to execute Swing application code in the appropriate thread, let's take a look at how threading is often implemented. We'll look at the standard mechanisms for moving code into and out of the Swing thread. In the process, I'll highlight some of the problems and difficulties with the standard approach. As we'll see, most of the problems come from attempting to implement a synchronous code model with the asynchronous Swing threading model. From there, we will see how to modify our example to be event-driven -- migrating the entire approach to an asynchronous model.

Common Swing Threading Solution

Let's start by looking at one of the most common Swing threading mistakes. We will try to fix this problem using the standard techniques. In the process, you will see the complexity and common difficulties of implementing correct Swing threading. Also, note that in the process of fixing this threading problem, many of the intermediate examples will also not work. In the example, I note where the code breaks with a code comment starting with //broken. So now, let's get to our example.

Assume that we are performing book searches. We have a simple user interface with a search text field, a search button, and an output text area. This interface is shown in Figure 1. Don't hold me to the UI design. This is pretty ugly, I agree.

Figure 1. Basic query UI
Figure 1. Basic query UI

The user enters a book title, author, or other criteria, and a list of results is displayed. The following code sample shows the button's ActionListener calling the lookup() method in the same thread. For these examples, I am using a stubbed out-lookup where I call thread.sleep() for five seconds. The result of the sleeping thread is the same as a synchronous server call that lasts five seconds.

private void searchButton_actionPerformed() {
    outputTA.setText("Searching for: " +
                     searchTF.getText());
    //Broken!! Too much work in the Swing thread
    String[] results = lookup(searchTF.getText());
    outputTA.setText("");
    for (int i = 0; i < results.length; i++) {
        String result = results[i];
        outputTA.setText(outputTA.getText() + 
                        '\n' + result);
    }
}

If you run this code (the complete source is available for download), there are a few things that you will immediately notice are wrong. Figure 2 shows a screen shot of the search running.

Figure 2. Doing search in the Swing thread
Figure 2. Doing the search in the Swing thread

Notice that the Go button appears "pressed." This is because the actionPerformed method, which notifies the button to be repainted in its non-pressed look, has not returned. Also, notice that the search string "abcde" is not displayed in the text area. The first line of the searchButton_actionPerformed method sets the text area text to the search string. But remember that Swing repaints are not immediate. Rather, a repaint request is placed on the Swing event queue for the Swing thread to process. But here, we are occupying the Swing thread with our lookup, so it can't process the repaint.

To fix these and other problems, let's move to the lookup to a non-Swing thread. The first tendency is to have the entire method execute in a new thread. The problem with this is that the Swing components, in this case the output text area, can only be edited from the Swing thread. Here is the modified searchButton_actionPerformed method:

private void searchButton_actionPerformed() {
    outputTA.setText("Searching for: " +
                     searchTF.getText());
    //the String[][] is used to allow access to
    // setting the results from an inner class
    final String[][] results = new String[1][1];
    new Thread(){
        public void run() {
           results[0] = lookup(searchTF.getText());
        }
    }.start();
    outputTA.setText("");
    for (int i = 0; i < results[0].length; i++) {
        String result = results[0][i];
        outputTA.setText(outputTA.getText() +
                        '\n' + result);
    }
}

There are a few problems with this. Notice the final String[][]. This is an unfortunate artifact involved with anonymous inner classes and scope. Basically, any variable used in an anonymous inner class but defined in the containing class scope needs to be declared final. You can get around this by making an array to hold the variable. This way, you can make the array final and modify the element in the array but not the array reference itself. Now that we're done with the minutiae, let's get to the real problem. Figure 3 shows what happens when this code is run:

Figure 3. Doing search outside the Swing thread
Figure 3. Doing the search outside of the Swing thread

The display shows a null because the display code is processed before the lookup code completes! This is because the code block continues execution once the new thread is started, not when it's done executing. This is one of those strange-looking concurrent code blocks where code later on in a method can actually execute before code earlier in a method.

There are two methods in the SwingUtilities class that can help us out here: invokeLater() and invokeAndWait(). Each method takes a Runnable and executes it in the Swing thread. The invokeAndWait() method blocks until the Runnable completes execution, and invokeLater() executes the Runnable asynchronously. Using invokeAndWait() is generally frowned upon, since it can cause severe thread deadlocks that can wreak havoc on your application. So let's just put it aside and use the invokeLater() method.

To fix the last problem with variable scooping and order of execution, we have to move the text-area getText() and setText() calls into a Runnable that executes only when the results are returned, and executes in the Swing thread. We can do this by creating an anonymous Runnable that we pass to invokeLater(), containing the text area manipulation from the end of the new Thread's Runnable. This guarantees that the Swing code will not execute before the lookup completes. Here is the code:

private void searchButton_actionPerformed() {
    outputTA.setText("Searching for: " +
                     searchTF.getText());
    final String[][] results = new String[1][1];
    new Thread() {
        public void run() {
            //get results.
            results[0] = lookup(searchTF.getText());
            // send runnable to the Swing thread
            // the runnable is queued after the
            // results are returned
            SwingUtilities.invokeLater(
                new Runnable() {
                    public void run() {
                        // Now we're in the Swing thread
                        outputTA.setText("");
                        for (int i = 0;
                             i < results[0].length;
                             i++) {
                            String result = results[0][i];
                            outputTA.setText(
                                outputTA.getText() +
                                '\n' + result);
                        }
                    }
                }
            );
        }
    }.start();
}

This will work. But it was a major headache to get here. We had to pay serious attention to the order of execution through anonymous Threads, and we had to deal with difficult scooping issues. These are not rare problems. Additionally, this is a pretty simple example, and we've already had major issues with scope, variable passing, and order of execution. Imagine more complex problems where there are several levels of nesting, with shared references and a designated order of execution. This approach quickly gets out of hand.

The Problem

We are trying to force synchronous execution through an asynchronous model -- trying to fit a square peg in a round hole. As long as we're trying to do this, we will continue to encounter these problems. From experience, I can tell you this code will be hard to write, hard to maintain, and very error-prone.

This seems like a common problem, so there must be standard ways to solve this, right? There are several frameworks including one that I wrote but was never publicly released. I called it the Chained Runnable Engine ad it suffered from similar synchronous-versus-asynchronous problems. Using this framework, you would create a collection of Runnables that would be executed by the engine. Each Runnable had an indicator telling the engine whether to execute it in the Swing thread or an alternate thread. The engine also ensured that each Runnable executed in proper order. So Runnable #2 would not be queued until Runnable #1 completed. And finally, it supported variable passing in the form of a HashMap that was passed from Runnable to Runnable.

On this surface, this looks like it solves our main problems. But once you start to dig deeper, the same problems arise. Essentially, we haven't changed anything from what has been described above -- we would just be hiding some of the complexity in the engine. The code was very tedious to write and was quite complex, due to a seemingly exponential number of Runnables, which often ended up being tightly coupled. The non-typed HashMap variable passing between Runnables became hard to manage. The list goes on.

After working on this framework, I realized this requires a completely different solution. This led me to reexamine the problem, look at how others are solving similar problems, and take a close look at the Swing source.

Pages: 1, 2

Next Page » 

View all java.net Articles.

 Feed java.net RSS Feeds