1.4 Concurrency in Swing

«« Previous
Next »»

This lesson discusses concurrency as it applies to Swing programming. It assumes that you are already familiar with the content of the Concurrency lesson in the Essential Classes trail.

Careful use of concurrency is particularly important to the Swing programmer. A well-written Swing program uses concurrency to create a user interface that never "freezes" — the program is always responsive to user interaction, no matter what it's doing. To create a responsive program, the programmer must learn how the Swing framework employs threads.

A Swing programmer deals with the following kinds of threads:
  • Initial threads, the threads that execute initial application code.
  • The event dispatch thread, where all event-handling code is executed. Most code that interacts with the Swing framework must also execute on this thread.
  • Worker threads, also known as background threads, where time-consuming background tasks are executed.
The programmer does not need to provide code that explicitly creates these threads: they are provided by the runtime or the Swing framework. The programmer's job is to utilize these threads to create a responsive, maintainable Swing program.

Like any other program running on the Java platform, a Swing program can create additional threads and thread pools, using the tools described in the Concurrency lesson. But for basic Swing programs the threads described here are sufficient.

This lesson discusses each of the three kinds of threads in turn. Worker threads require the most discussion because tasks that run on them are created using javax.swing.SwingWorker. This class has many useful features, including communication and coordination between worker thread tasks and the tasks on other threads.

Initial Threads

Every program has a set of threads where the application logic begins. In standard programs, there's only one such thread: the thread that invokes the main method of the program class. In applets the initial threads are the ones that construct the applet object and invoke its init and start methods; these actions may occur on a single thread, or on two or three different threads, depending on the Java platform implementation. In this lesson, we call these threads the initial threads.

In Swing programs, the initial threads don't have a lot to do. Their most essential job is to create a Runnable object that initializes the GUI and schedule that object for execution on the event dispatch thread. Once the GUI is created, the program is primarily driven by GUI events, each of which causes the execution of a short task on the event dispatch thread. Application code can schedule additionals tasks on the event dispatch thread (if they complete quickly, so as not to interfere with event processing) or a worker thread (for long-running tasks).

An initial thread schedules the GUI creation task by invoking javax.swing.SwingUtilities.invokeLater or javax.swing.SwingUtilities.invokeAndWait . Both of these methods take a single argument: the Runnable that defines the new task. Their only difference is indicated by their names: invokeLater simply schedules the task and returns; invokeAndWait waits for the task to finish before returning.

You can see examples of this throughout the Swing tutorial:

SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        createAndShowGUI();
    }
});

In an applet, the GUI-creation task must be launched from the init method using invokeAndWait; otherwise, init may return before the GUI is created, which may cause problems for a web browser launching an applet. In any other kind of program, scheduling the GUI-creation task is usually the last thing the initial thread does, so it doesn't matter whether it uses invokeLater or invokeAndWait.

Why does not the initial thread simply create the GUI itself? Because almost all code that creates or interacts with Swing components must run on the event dispatch thread. This restriction is discussed further in the next section.

The Event Dispatch Thread

Swing event handling code runs on a special thread known as the event dispatch thread. Most code that invokes Swing methods also runs on this thread. This is necessary because most Swing object methods are not "thread safe": invoking them from multiple threads risks thread interference or memory consistency errors. Some Swing component methods are labelled "thread safe" in the API specification; these can be safely invoked from any thread. All other Swing component methods must be invoked from the event dispatch thread. Programs that ignore this rule may function correctly most of the time, but are subject to unpredictable errors that are difficult to reproduce.

It's useful to think of the code running on the event dispatch thread as a series of short tasks. Most tasks are invocations of event-handling methods, such as ActionListener.actionPerformed. Other tasks can be scheduled by application code, using invokeLater or invokeAndWait. Tasks on the event dispatch thread must finish quickly; if they don't, unhandled events back up and the user interface becomes unresponsive.

If you need to determine whether your code is running on the event dispatch thread, invoke javax.swing.SwingUtilities.isEventDispatchThread.

Worker Threads and SwingWorker

When a Swing program needs to execute a long-running task, it usually uses one of the worker threads, also known as the background threads. Each task running on a worker thread is represented by an instance of javax.swing.SwingWorker. SwingWorker itself is an abstract class; you must define a subclass in order to create a SwingWorker object; anonymous inner classes are often useful for creating very simple SwingWorker objects.

SwingWorker provides a number of communication and control features:
  • The SwingWorker subclass can define a method, done, which is automatically invoked on the event dispatch thread when the background task is finished.
  • SwingWorker implements java.util.concurrent.Future. This interface allows the background task to provide a return value to the other thread. Other methods in this interface allow cancellation of the background task and discovering whether the background task has finished or been cancelled.
  • The background task can provide intermediate results by invoking SwingWorker.publish, causing SwingWorker.process to be invoked from the event dispatch thread.
  • The background task can define bound properties. Changes to these properties trigger events, causing event-handling methods to be invoked on the event dispatch thread.
These features are discussed in the following subsections.

Note:  The javax.swing.SwingWorker class was added to the Java platform in Java SE 6. Prior to this, another class, also called SwingWorker, was widely used for some of the same purposes. The old SwingWorker was not part of the Java platform specification, and was not provided as part of the JDK.

The new javax.swing.SwingWorker is a completely new class. Its functionality is not a strict superset of the old SwingWorker. Methods in the two classes that have the same function do not have the same names. Also, instances of the old SwingWorker class were reusable, while a new instance of javax.swing.SwingWorker is needed for each new background task.

Throughout the Java Tutorials, any mention of SwingWorker now refers to javax.swing.SwingWorker.

Simple Background Tasks

Let's start with a task that is very simple, but potentially time-consuming. The TumbleItem applet loads a set of graphic files used in an animation. If the graphic files are loaded from an initial thread, there may be a delay before the GUI appears. If the graphic files are loaded from the event dispatch thread, the GUI may be temporarily unresponsive.

To avoid these problems, TumbleItem creates and executes an instance of SwingWorker from its initial threads. The object's doInBackground method, executing in a worker thread, loads the images into an ImageIcon array, and returns a reference to it. Then the done method, executing in the event dispatch thread, invokes get to retrieve this reference, which it assigns to an applet class field named imgs. This allows TumbleItem to construct the GUI immediately, without waiting for the images to finish loading.

Here is the code that defines and executes the SwingWorker object.

SwingWorker worker = new SwingWorker<ImageIcon[], Void>() {
    @Override
    public ImageIcon[] doInBackground() {
        final ImageIcon[] innerImgs = new ImageIcon[nimgs];
        for (int i = 0; i < nimgs; i++) {
            innerImgs[i] = loadImage(i+1);
        }
        return innerImgs;
    }

    @Override
    public void done() {
        //Remove the "Loading images" label.
        animator.removeAll();
        loopslot = -1;
        try {
            imgs = get();
        } catch (InterruptedException ignore) {}
        catch (java.util.concurrent.ExecutionException e) {
            String why = null;
            Throwable cause = e.getCause();
            if (cause != null) {
                why = cause.getMessage();
            } else {
                why = e.getMessage();
            }
            System.err.println("Error retrieving file: " + why);
        }
    }
};

All concrete subclasses of SwingWorker implement doInBackground; implementation of done is optional.

Notice that SwingWorker is a generic class, with two type parameters. The first type parameter specifies a return type for doInBackground, and also for the get method, which is invoked by other threads to retrieve the object returned by doInBackground. SwingWorker's second type parameter specifies a type for interim results returned while the background task is still active. Since this example doesn't return interim results, Void is used as a placeholder.

You may wonder if the code that sets imgs is unnecessarily complicated. Why make doInBackground return an object and use done to retrieve it? Why not just have doInBackground set imgs directly? The problem is that the object imgs refers to is created in the worker thread and used in the event dispatch thread. When objects are shared between threads in this way, you must make sure that changes made in one thread are visible to the other. Using get guarantees this, because using get creates a happens before relationship between the code that creates imgs and the code that uses it. For more on the happens before relationship, refer to Memory Consistency Errors in the Concurrency lesson.

There are actually two ways to retrieve the object returned by doInBackground.
  • Invoke SwingWorker.get with no arguments. If the background task is not finished, get blocks until it is.
  • Invoke SwingWorker.get with arguments indicating a timeout. If the background task is not finished, get blocks until it is — unless the timeout expires first, in which case get throws java.util.concurrent.TimeoutException.
Be careful when invoking either overload of get from the event dispatch thread; until get returns, no GUI events are being processed, and the GUI is "frozen". Don't invoke get without arguments unless you are confident that the background task is complete or close to completion.

Tasks that Have Interim Results

It is often useful for a background task to provide interim results while it is still working. The task can do this by invoking SwingWorker.publish. This method accepts a variable number of arguments. Each argument must be of the type specified by SwingWorker's second type parameter.

To collect results provided by publish, override SwingWorker.process This method will be invoked from the event dispatch thread. Results from multiple invocations of publish are often accumulated for a single invocation of process.

This program tests the fairness of java.util.Random by generating a series of random boolean values in a background task. This is equivalent to flipping a coin; hence the name Flipper. To report its results, the background task uses an object of type FlipPair

private static class FlipPair {
    private final long heads, total;
    FlipPair(long heads, long total) {
        this.heads = heads;
        this.total = total;
    }
}

The heads field is the number of times the random value has been true; the total field is the total number of random values.

The background task is represented by an instance of FlipTask:

private class FlipTask extends SwingWorker<Void, FlipPair> {
Since the task does not return a final result, it does not matter what the first type parameter is; Void is used as a placeholder. The task invokes publish after each "coin flip":

@Override
protected Void doInBackground() {
    long heads = 0;
    long total = 0;
    Random random = new Random();
    while (!isCancelled()) {
        total++;
        if (random.nextBoolean()) {
            heads++;
        }
        publish(new FlipPair(heads, total));
    }
    return null;
}

(The isCancelled method is discussed in the next section.) Because publish is invoked very frequently, a lot of FlipPair values will probably be accumulated before process is invoked in the event dispatch thread; process is only interested in the last value reported each time, using it to update the GUI:

protected void process(List<FlipPair> pairs) {
    FlipPair pair = pairs.get(pairs.size() - 1);
    headsText.setText(String.format("%d", pair.heads));
    totalText.setText(String.format("%d", pair.total));
    devText.setText(String.format("%.10g",
            ((double) pair.heads)/((double) pair.total) - 0.5));
}

If Random is fair, the value displayed in devText should get closer and closer to 0 as Flipper runs.

Note: The setText method used in Flipper is actually "thread safe" as defined in its specification. That means that we could dispense with publish and process and set the text fields directly from the worker thread. We've chosen to ignore this fact in order to provide a simple demonstration of SwingWorker interim results.

Canceling Background Tasks

To cancel a running background task, invoke SwingWorker.cancel The task must cooperate with its own cancellation. There are two ways it can do this:

  • By terminating when it receives an interrupt. This procedures is described in Interrupts in Concurrency.
  • By invoking SwingWorker.isCancelled at short intervals. This method returns true if cancel has been invoked for this SwingWorker.

The cancel method takes a single boolean argument. If the argument is true, cancel sends the background task an interrupt. Whether the argument is true or false, invoking cancel changes the cancellation status of the object to true. This is the value returned by isCancelled. Once changed, the cancellation status cannot be changed back.

The Flipper example from the previous section uses the status-only idiom. The main loop in doInBackground exits when isCancelled returns true. This will occur when the user clicks the "Cancel" button, triggering code that invokes cancel with an argument of false.

The status-only approach makes sense for Flipper because its implementation of SwingWorker.doInBackground does not include any code that might throw InterruptedException. To respond to an interrupt, the background task would have to invoke Thread.isInterrupted at short intervals. It's just as easy to use SwingWorker.isCancelled for the same purpose

Note: If get is invoked on a SwingWorker object after its background task has been cancelled, java.util.concurrent.CancellationException is thrown.

Bound Properties and Status Methods

SwingWorker supports bound properties, which are useful for communicating with other threads. Two bound properties are predefined: progress and state. As with all bound properties, progress and state can be used to trigger event-handling tasks on the event dispatch thread.

By implementing a property change listener, a program can track changes to progress, state, and other bound properties.

The progress Bound Variable

The progress bound variable is an int value that can range from 0 to 100. It has a predefined setter method (the protected SwingWorker.setProgress) and a predefined getter method (the public SwingWorker.getProgress).

The ProgressBarDemo example uses progress to update a ProgressBar control from a background task.

The state Bound Variable

The state bound variable indicates where the SwingWorker object is in its lifecycle. The bound variable contains an enumeration value of type SwingWorker.StateValue. Possible values are:

PENDING
The state during the period from the construction of the object until just before doInBackground is invoked.
STARTED
The state during the period from shortly before doInBackground is invoked until shortly before done is invoked.
DONE
The state for the remainder of the existence of the object.
The current value of the state bound variable is returned by SwingWorker.getState.

Status Methods

Two methods, part of the Future interface, also report on the status of the background task. As we saw in Canceling Background Tasks, isCancelled returns true if the task has been canceled. In addition, isDone returns true if the task has finished, either normally, or by being cancelled.

Answers: Concurrency in Swing

Questions

Q1. For each of the following tasks, specify which thread it should be executed in and why.
Ans:
  • Initializing the GUI. The event dispatch thread; most interactions with the GUI framework must occur on this thread.
  • Loading a large file. A worker thread. Executing this task on the event dispatch thread would prevent GUI events from being processed, "freezing" the GUI until the task is finished. Executing this task on an initial thread would cause a delay in creating the GUI.
  • Invoking javax.swing.JComponent.setFont to change the font of a component. The event dispatch thread. As with most Swing methods, it is not safe to invoke setFont from any other thread.
  • Invoking javax.swing.text.JTextComponent.setText to change the text of a component. This method is documented as thread-safe, so it can be invoked from any thread.
Q2. One thread is not the preferred thread for any of the tasks mentioned in the previous question. Name this thread and explain why its applications are so limited.
Ans: The initial threads launch the first GUI task on the event dispatch thread. After that, a Swing program is primarily driven by GUI events, which trigger tasks on the event dispatch thread and the worker thread. Usually, the initial threads are left with nothing to do.

Q3: SwingWorker has two type parameters. Explain how these type parameters are used, and why it often doesn't matter what they are.
Ans: The type parameters specify the type of the final result (also the return type of the doInBackground method) and the type of interim results (also the argument types for publish and process). Many background tasks do not provide final or interim results.

Exercises

Q1: Modify the Flipper example so that it pauses 5 seconds between "coin flips." If the user clicks the "Cancel", the coin-flipping loop terminates immediately.
Ans: The modified program adds a delay in the central doInBackground loop:

protected Object doInBackground() {
    long heads = 0;
    long total = 0;
    Random random = new Random();
    while (!isCancelled()) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            //Cancelled!
            return null;
        }
        total++;
        if (random.nextBoolean()) {
            heads++;
        }
        publish(new FlipPair(heads, total));
    }
    return null;
}

package QandE;

import java.util.List;
import java.util.Random;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.Insets;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.BorderFactory;
import javax.swing.SwingWorker;

public class Flipper2 extends JFrame 
                  implements ActionListener {
    GridBagConstraints constraints;
    JTextField headsText, totalText, ratioText;
    Border border =
        BorderFactory.createLoweredBevelBorder();
    JButton startButton, stopButton;
    FlipTask flipTask;

    private JTextField makeText() {
        JTextField t = new JTextField(20);
        t.setEditable(false);
        t.setHorizontalAlignment(JTextField.RIGHT);
        t.setBorder(border);
        getContentPane().add(t, constraints);
        return t;
    }

    private JButton makeButton(String caption) {
        JButton b = new JButton(caption);
        b.setActionCommand(caption);
        b.addActionListener(this);
        getContentPane().add(b, constraints);
        return b;
    }
    public Flipper2() {
        super("Flipper");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Make text boxes
        getContentPane().setLayout(new GridBagLayout());
        constraints = new GridBagConstraints();
        constraints.insets = new Insets(3, 10, 3, 10);
        headsText = makeText();
        totalText = makeText();
        ratioText = makeText();

        //Make buttons
        startButton = makeButton("Start");
        stopButton = makeButton("Stop");
        stopButton.setEnabled(false);

        //Display the window.
        pack();
        setVisible(true);
    }

    class FlipPair {
        long heads, total;
        FlipPair(long heads, long total) {
            this.heads = heads;
            this.total = total;
        }
    }

    class FlipTask extends SwingWorker<Object, FlipPair> {
        protected Object doInBackground() {
            long heads = 0;
            long total = 0;
            Random random = new Random();
            while (!isCancelled()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    //Cancelled!
                    return null;
                }
                total++;
                if (random.nextBoolean()) {
                    heads++;
                }
                publish(new FlipPair(heads, total));
            }
            return null;
        }

        protected void process(List<FlipPair> pairs) {
            FlipPair pair = pairs.get(pairs.size() - 1);
            headsText.setText(String.format("%d", pair.heads));
            totalText.setText(String.format("%d", pair.total));
            ratioText.setText(String.format("%g", 
                    ((double) pair.heads)/((double) pair.total)));
        }
    }


    public void actionPerformed(ActionEvent e) {
        //Only two buttons: if not "Start" then "Stop"
        boolean startMode = e.getActionCommand().equals("Start");
        startButton.setEnabled(!startMode);
        stopButton.setEnabled(startMode);
        if (startMode) {
            flipTask = new FlipTask();
            flipTask.execute();
        } else {
            flipTask.cancel(true);
            flipTask = null;
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Flipper2();
            }
        });
    }
}

The try ... catch causes doInBackground to return if an interrupt is received while the thread is sleeping. Invoking cancel with an argument of true ensures that an interrupt is sent when the task is cancelled.

«« Previous
Next »»