Surprising SwingWorker Behavior

SwingWorker makes it easy to execute background tasks and report progress to the Event Dispatch Thread (EDT). Once on the EDT, your code can safely update GUI components like progress bars. This figure shows the general idea:

SwingWorker Threads

The general steps in my example are:

  • Extend the SwingWorker class.
  • Override doInBackground() to perform your background task.
  • While working, periodically call publish(...).
  • The SwingWorker will, in turn, periodically call process(...) on the EDT.
  • You can override process(...) to update the GUI as work proceeds.
  • You override done() to know when work completes. (or so you may think)

It turns out done() does not really work like that. Let’s look at some Java 6 code.

Sample Code

I’ll just present the entire application. This loops through fifty iterations in a background thread, pausing for random intervals between each iteration. It publishes the results and prints them to the console in the process(...) method. The done() method prints a message as well. Here it is:

public class Demo extends JFrame {
  private Random rand = new Random();

  private AbstractAction startAction = new AbstractAction("Start") {
    public void actionPerformed(ActionEvent e) {
      startClicked();
    }
  };

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

  public Demo() {
    super("SwingWorker Demo");
    add(new JButton(startAction), BorderLayout.CENTER);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    pack();
    setLocationByPlatform(true);
  }

  private void startClicked() {
    startAction.setEnabled(false);
    new DemoWorker().execute();
  }

  private class DemoWorker extends SwingWorker<String, String> {
    protected String doInBackground() throws Exception {
      for (int i = 0; i < 50; i++) {
        publish(Integer.toString(i));
        TimeUnit.MILLISECONDS.sleep(rand.nextInt(25));
      }
      return null;
    }

    @Override
    protected void process(List<String> chunks) {
      System.out.println("process: " + chunks);
    }

    @Override
    protected void done() {
      System.out.println("done.");
      startAction.setEnabled(true);
    }
  }
}

Expected Output

Each time you run the program, you see slightly different results. That’s because publish(...) coalesces events and only calls process(...) “occasionally”. This is a really nice feature because it helps avoid bogging down the EDT. Here is the output:

process: [0, 1, 2, 3]
process: [4, 5]
process: [6, 7, 8]
process: [9, 10, 11, 12]
process: [13, 14, 15, 16]
process: [17, 18, 19]
process: [20, 21]
process: [22, 23, 24, 25]
process: [26, 27, 28, 29]
process: [30, 31, 32]
process: [33, 34, 35, 36]
process: [37, 38, 39, 40, 41]
process: [42, 43, 44]
process: [45, 46]
process: [47, 48, 49]
done.

See? Just what we expected. Let’s run it again:

process: [0, 1, 2, 3]
process: [4, 5]
process: [6, 7, 8, 9]
process: [10, 11, 12, 13]
process: [14, 15]
process: [16, 17, 18, 19]
process: [20, 21, 22, 23]
process: [24, 25]
process: [26, 27, 28]
process: [29, 30, 31, 32, 33]
process: [34, 35, 36]
process: [37, 38, 39]
process: [40, 41, 42]
process: [43, 44, 45]
process: [46, 47]
done.
process: [48, 49]

Surprising Results

What just happened? It turns out process(...) is sometimes called after done()! You don’t really have any control over this and cannot predict when it will happen.

This is not a bug. The JavaDocs for SwingWorker only say this for the done() method:

Executed on the Event Dispatch Thread after the doInBackground() method is finished…

All we really know is that done() is called “sometime” after doInBackground() completes.

Lesson Learned

You cannot rely on the order of process(...) and done(). A co-worker discovered this last week and brought this to my attention. If your process(...) method simply prints some message to the screen or updates a progress bar, this phenomenon is probably harmless. But in his application, his done() method actually changed the state on some data, but then (to his surprise) process(...) was called later, which caused a problem.

If you really need to know when your SwingWorker is done, and if you override both done() and process(...), you must come up with your own protocol to definitively know when your worker really is “done”.

For instance, your doInBackground() method might set a volatile boolean flag to true upon completion. Or perhaps the message you send to publish() can include a completion status indicator.


Igor Kushnirskiy Says:

Hi,

There was a bug on this. 6493680 [SwingWorker notifications might be out of order.] http://bugs.sun.com/view_bug.do?bug_id=6493680. This bug is fixed in 6u1 and 7 releases.

Thanks,
Igor