SwingWorker: asynchronous solution
Solutions have been proposed for the GUI freeze problem; asynchronous solutions
rely on the combined usage of a worker thread and the SwingUtilities.invokeLater() method. We will see in few lines why
they're called asynchronous.
The main idea behind asynchronous solution is to return quickly from the time-consuming listener, after having
delegated the time-consuming task to a worker thread. The worker thread has to do 2 things:
- Execute the time-consuming task
- Post an event to the Event Queue using
SwingUtilities.invokeLater()
Take a look at the code below which uses the SwingWorker, which is an asynchronous solution.
Let's concentrate on the button's listener (the actionPerformed() method): the first statement, as in the freeze example, changes
the text of the button and thus posts a repaint event to the Event Queue.
The next statement creates a SwingWorker object and starts it. This operation is quick, and non blocking.
When a SwingWorker is started, a worker thread is also started for executing the code contained in construct() ;
when the construct() method ends, the finished() method is called (using
SwingUtilities.invokeLater() ) and executed in the Event Dispatch Thread.
So we create the SwingWorker, we start it, the listener finishes and returns; the Event Dispatch Thread
can thus dequeue the next event and process it (very likely this event is the one posted by the first statement,
that changes the button's text to "Sleeping...") .
When the worker thread finishes, the finished() method is posted as event in the Event Queue, and again the
Event Dispatch Thread can dequeue it and process it, finally calling finished() .
This is why these solutions are called asynchronous: they let the event listener return immediately, and the code the
listener is supposed to execute is run asynchronously, while the listener still returns.
This solution, while resolving the freeze problem, has several drawbacks:
- Note the ugly exception handling. Even in this simple example, 3 chained if-else statements are required. Furthermore,
the exception handling is done inside the SwingWorker, not inside the listener: there is no simple way to rethrow exceptions
(it is possible but needs more coding).
- Note the asymmetry: the first
setText() is made outside the SwingWorker, the second inside of it
- Note the confusing
get() method: if construct() returns null (because the operation had a void return
value - like Thread.sleep() ), it is easy to forget to call get() to see if any exception was thrown by the
time-consuming code.
- What happens if the time-consuming code stays the same, but we want to execute 2 different
finished() methods
depending on the place from where we want to execute the time-consuming task ?
- If some code is written after
SwingWorker.start() , it will be always executed before finished() .
Thus looking at the code, we see:
setText("Sleeping...")
Thread.sleep()
setText("Slept !");
somethingElse()
but the real order of execution is:
setText("Sleeping...")
somethingElse() [before, concurrently or after Thread.sleep() ]
Thread.sleep()
setText("Slept !");
making debugging and code readability very difficult.
This is why a golden rule of the SwingWorker is to never put code after SwingWorker.start() .
- If the code inside
finished() requires a new time-consuming operation, a new nested SwingWorker should be used,
making the code complex and obscure, especially with respect to the sequence order of the operations executed.
Fortunately, Foxtrot solves these issues.
public class AsyncExample extends JFrame
{
public static void main(String[] args)
{
AsyncExample example = new AsyncExample();
example.setVisible(true);
}
public AsyncExample()
{
super("SwingWorker Example");
final JButton button = new JButton("Take a nap !");
button.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
button.setText("Sleeping...");
new SwingWorker()
{
protected Object construct() throws Exception
{
Thread.sleep(10000);
return "Slept !";
}
protected void finished()
{
try
{
String text = (String)get();
button.setText(text);
}
catch (InterruptedException ignored) {}
catch (InvocationTargetException x)
{
// Do exception handling
Throwable t = x.getException();
if (t instanceof InterruptedException) ...
else if (t instanceof RuntimeException) ...
else if (t instanceof Error) ...
}
}
}.start();
somethingElse();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
Container c = getContentPane();
c.setLayout(new GridBagLayout());
c.add(button);
setSize(300,200);
Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
Dimension size = getSize();
int x = (screen.width - size.width) >> 1;
int y = (screen.height - size.height) >> 1;
setLocation(x, y);
}
}
|
Legend |
Main Thread |
Event Dispatch Thread |
SwingWorker Thread |
|
|