Multi-threading in Java Swing with SwingWorker
Join the DZone community and get the full member experience.
Join For FreeIf you're writing a desktop or Java Web Start program in Java using Swing, you might feel the need to run some stuff in the background by creating your own threads.
There's nothing stopping you from using standard multi-threading techniques in Swing, and the usual considerations apply. If you have multiple threads accessing the same variables, you'll need to use synchronized methods or code blocks (or thread-safe classes like AtomicInteger or ArrayBlockingQueue).
However, there is a pitfall for the unwary. As with most user interface APIs, you can't update the user interface from threads you've created yourself. Well, as every Java undergraduate knows, you often can, but you shouldn't. If you do this, sometimes your program will work and other times it won't.
You can get around this problem by using the specialised SwingWorker class. In this article, I'll show you how you can get your programs working even if you're using the Thread class, and then we'll go on to look at the SwingWorker solution.
For demonstration purposes, I've created a little Swing program.
As you can see, it consists of two labels and a start button. At the moment, clicking the start button invokes a handler method which does nothing. Here's the Java code:
import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.List; import java.util.concurrent.ExecutionException; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; public class MainFrame extends JFrame { private JLabel countLabel1 = new JLabel("0"); private JLabel statusLabel = new JLabel("Task not completed."); private JButton startButton = new JButton("Start"); public MainFrame(String title) { super(title); setLayout(new GridBagLayout()); countLabel1.setFont(new Font("serif", Font.BOLD, 28)); GridBagConstraints gc = new GridBagConstraints(); gc.fill = GridBagConstraints.NONE; gc.gridx = 0; gc.gridy = 0; gc.weightx = 1; gc.weighty = 1; add(countLabel1, gc); gc.gridx = 0; gc.gridy = 1; gc.weightx = 1; gc.weighty = 1; add(statusLabel, gc); gc.gridx = 0; gc.gridy = 2; gc.weightx = 1; gc.weighty = 1; add(startButton, gc); startButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent arg0) { start(); } }); setSize(200, 400); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); } private void start() { } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new MainFrame("SwingWorker Demo"); } }); } }
We're going to add some code into the start() method which is called in response to the start button being clicked.
First let's try a normal thread.
private void start() { Thread worker = new Thread() { public void run() { // Simulate doing something useful. for(int i=0; i<=10; i++) { // Bad practice countLabel1.setText(Integer.toString(i)); try { Thread.sleep(1000); } catch (InterruptedException e) { } } // Bad practice statusLabel.setText("Completed."); } }; worker.start(); }
As a matter of fact, this code seems to work (at least for me anyway). The program ends up looking like this:
This isn't recommended practice, however. We're updating the GUI from our own thread, and under some circumstances that will certainly cause exceptions to be thrown.
If we want to update the GUI from another thread, we should use SwingUtilities to schedule our update code to run on the event dispatch thread.
The following code is fine, but ugly as the devil himself.
private void start() { Thread worker = new Thread() { public void run() { // Simulate doing something useful. for(int i=0; i<=10; i++) { final int count = i; SwingUtilities.invokeLater(new Runnable() { public void run() { countLabel1.setText(Integer.toString(count)); } }); try { Thread.sleep(1000); } catch (InterruptedException e) { } } SwingUtilities.invokeLater(new Runnable() { public void run() { statusLabel.setText("Completed."); } }); } }; worker.start(); }
Surely there must be something we can do to make our code more elegant?
The SwingWorker Class
SwingWorker is an alternative to using the Thread class, specifically designed for Swing. It's an abstract class and it takes two template parameters, which make it look highly ferocious and puts most people off using it. But in fact it's not as complex as it seems.
Let's take a look at some code that just runs a background thread. For this first example, we won't be using either of the template parameters, so we'll set them both to Void, Java's class equivalent of the primitive void type (with a lower-case 'v').
Running a Background Task
We can run a task in the background by implementing the doInBackground method and calling execute to run our code.
SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() { @Override protected Void doInBackground() throws Exception { // Simulate doing something useful. for (int i = 0; i <= 10; i++) { Thread.sleep(1000); System.out.println("Running " + i); } return null; } }; worker.execute();
Note that SwingWorker is a one-shot affair, so if we want to run the code again, we'd need to create another SwingWorker; you can't restart the same one.
Pretty simple, hey? But what if we want to update the GUI with some kind of status after running our code? You cannot update the GUI from doInBackground, because it's not running in the main event dispatch thread.
But there is a solution. We need to make use of the first template parameter.
Updating the GUI After the Thread Completes
We can update the GUI by returning a value from doInBackground() and then over-riding done(), which can safely update the GUI. We use the get() method to retrieve the value returned from doInBackground()
So the first template parameter determines the return type of both doInBackground() and get().
SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() { @Override protected Boolean doInBackground() throws Exception { // Simulate doing something useful. for (int i = 0; i <= 10; i++) { Thread.sleep(1000); System.out.println("Running " + i); } // Here we can return some object of whatever type // we specified for the first template parameter. // (in this case we're auto-boxing 'true'). return true; } // Can safely update the GUI from this method. protected void done() { boolean status; try { // Retrieve the return value of doInBackground. status = get(); statusLabel.setText("Completed with status: " + status); } catch (InterruptedException e) { // This is thrown if the thread's interrupted. } catch (ExecutionException e) { // This is thrown if we throw an exception // from doInBackground. } } }; worker.execute();
What if we want to update the GUI as we're going along? That's what the second template parameter is for.
Updating the GUI from a Running Thread
To update the GUI from a running thread, we use the second template parameter. We call the publish() method to 'publish' the values with which we want to update the user interface (which can be of whatever type the second template parameter specifies). Then we override the process() method, which receives the values that we publish.
Actually process() receives lists of published values, because several values may get published before process() is actually called.
In this example we just publish the latest value to the user interface.
SwingWorker<Boolean, Integer> worker = new SwingWorker<Boolean, Integer>() { @Override protected Boolean doInBackground() throws Exception { // Simulate doing something useful. for (int i = 0; i <= 10; i++) { Thread.sleep(1000); // The type we pass to publish() is determined // by the second template parameter. publish(i); } // Here we can return some object of whatever type // we specified for the first template parameter. // (in this case we're auto-boxing 'true'). return true; } // Can safely update the GUI from this method. protected void done() { boolean status; try { // Retrieve the return value of doInBackground. status = get(); statusLabel.setText("Completed with status: " + status); } catch (InterruptedException e) { // This is thrown if the thread's interrupted. } catch (ExecutionException e) { // This is thrown if we throw an exception // from doInBackground. } } @Override // Can safely update the GUI from this method. protected void process(List<Integer> chunks) { // Here we receive the values that we publish(). // They may come grouped in chunks. int mostRecentValue = chunks.get(chunks.size()-1); countLabel1.setText(Integer.toString(mostRecentValue)); } }; worker.execute();
More .... ? You Want More .... ?
I hope you enjoyed this introduction to the highly-useful SwingWorker class.
You can find more tutorials, including a complete free video course on multi-threading and courses on Swing, Android and Servlets, on my site Cave of Programming.
Until next time .... happy coding.
- John
Meta: this post is part of the Java Advent Calendar and is licensed under the Creative Commons 3.0 Attribution license. If you like it, please spread the word by sharing, tweeting, FB, G+ and so on! Want to write for the blog? We are looking for contributors to fill all 24 slot and would love to have your contribution! Contact Attila Balazs to contribute!
Published at DZone with permission of Java Advent, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments