Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

JavaWorld Daily Brew

Java Tutor

Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.


Event-Dispatch Thread Rules for Swing UIs

 

The event-dispatch thread (EDT) is a special background thread for processing events that originate from the Abstract Window Toolkit's event queue. Update events cause user interface (UI) components to redraw themselves, and input events originate from input devices such as the mouse and keyboard.

Swing is a single-threaded UI toolkit that uses the EDT to update and send input events to components. Because you must be careful in how you interact with this thread to ensure that your Swing applications work correctly, this tutorial presents and explains two rules that you need to follow to avoid potential problems.

Always Create Swing UIs on the EDT

One consequence of Swing being single-threaded is that you must create a Swing application's UI on the EDT only. It is incorrect to create this UI on any other thread, such as the main thread that runs a Java application, and which I demonstrate in Listing 1's HelloSwing application source code.

// HelloSwing.java

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class HelloSwing
{
   public static void main(String[] args)
   {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setContentPane (new JLabel("Hello, Swing", JLabel.CENTER));
      frame.pack();
      frame.setVisible(true);
   }
}

Listing 1: HelloSwing.java (version 1)

main(String[]) first instantiates the javax.swing.JFrame class to create a frame window in which to house the UI. It then invokes this class's setDefaultCloseOperation(int) method with the EXIT_ON_CLOSE constant to tell Swing to terminate the application when the user clicks the x button on the menubar.

main(String[]) next instantiates javax.swing.JLabel to create a Swing label component with some text -- JLabel.CENTER informs this component to center this text. The label component is then passed to JFrame's setContentPane(Container) method to establish the label and the frame window's content.

Finally, main(String[]) calls JFrame's pack() method to pack the frame window's content to its preferred size, which is just large enough to display the label. It also calls JFrame's setVisible(boolean) method with a true argument to realize (make visible) the frame window and its label content.

This code looks okay and appears to work properly, so why is it incorrect? Because Swing is single-threaded, most Swing objects (such as JFrame objects) are not thread-safe. Accessing these objects from multiple threads risks thread interference and/or memory inconsistency errors:

  • Thread interference: Two threads are performing two different operations but acting on the same data. For example, one thread reads a long integer counter variable while another thread updates that variable. Because a long integer being read/written on a 32-bit machine requires two read/write accesses, it is possible that the reading thread reads part of this variable's current value, the writing thread updates the variable, and then the reading thread reads the rest of the variable. The result is that the reading thread has an incorrect value.
  • Memory inconsistency errors: Two threads have inconsistent views of the same data. For example, the writing thread updates counter and then the reading thread reads this variable. However, it doesn't obtain the value that was written because each thread has its own local copy of the variable (through a caching mechanism being used to boost performance).

How might these problems occur when the UI is not created on the EDT (as demonstrated in Listing 1)? John Zukowski demonstrates one scenario in his JavaWorld article titled Swing threading and the event-dispatch thread.

Zukowski presents an example that adds a container listener to a frame window container component. Listener methods are called when a component is added to or removed from the frame. He demonstrates the EDT running code within a listener method before the frame window is realized on the main thread.

After the EDT starts to run in a listener method, and while the main thread continues to initialize the UI, components could be created by the main thread and accessed by the EDT. The EDT might attempt to access these components before they exist, and doing so could crash the application.

Even if the main thread creates the components before the EDT accesses them from the listener method, the EDT may have an inconsistent view (because of caching), and is unable to access the references to the new components. A program crash (probably a thrown NullPointerException instance) would most likely occur.

You can prevent these and other problems by offloading UI creation to the EDT. Accomplish this task by placing the UI creation code in a runnable (an object whose class implements the java.lang.Runnable interface) and passing this runnable to the javax.swing.SwingUtilities or java.awt.EventQueue class's invokeLater(Runnable) method, as demonstrated in Listing 2.

// HelloSwing.java

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class HelloSwing
{
   public static void main(String[] args)
   {
      Runnable r = new Runnable()
                   {
                      public void run()
                      {
                         JFrame frame = new JFrame();
                         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                         frame.setContentPane (new JLabel("Hello, Swing",
                                                          JLabel.CENTER));
                         frame.pack();
                         frame.setVisible(true);
                      }
                   };
      EventQueue.invokeLater(r);
   }
}

Listing 2: HelloSwing.java (version 2)

HelloSwing's main(String[]) method first creates a runnable, by instantiating an anonymous class that implements Runnable. It then passes this instance to EventQueue's invokeLater(Runnable) method. At some future point, Swing's EDT will invoke the runnable's run() method to create the UI.

Note: The SwingUtilities class's invokeLater(Runnable) method is a wrapper that calls the same-named method in the EventQueue class. Zukowski presents the reason for this architecture in the aforementioned article.

SwingUtilities provides many other useful methods, including isEventDispatchThread() for determining whether the current thread is the EDT or not, and invokeAndWait(Runnable) for invoking the runnable on the EDT and waiting until the runnable finishes. As with invokeLater(Runnable), these methods are wrappers for same-named EventQueue methods.

Never Delay the EDT

Another consequence of Swing being single-threaded is that you must never perform a lengthy activity on the EDT because doing so can make a Swing application's UI unresponsive -- the user cannot even close the application's main window. For example, consider Listing 3's ViewPage application source code.

// ViewPage.java

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.io.InputStream;
import java.io.IOException;

import java.net.URL;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class ViewPage
{
   public static void main(String[] args)
   {
      Runnable r;
      r = new Runnable()
          {
             public void run()
             {
                final JFrame frame = new JFrame("View Page");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JPanel panel = new JPanel();
                panel.add(new JLabel("Enter URL"));
                final JTextField txtURL = new JTextField(40);
                panel.add(txtURL);
                frame.getContentPane().add(panel, BorderLayout.NORTH);
                final JTextArea txtHTML = new JTextArea(10, 40);
                frame.getContentPane().add(new JScrollPane (txtHTML),
                                           BorderLayout.CENTER);
                ActionListener al;
                al = new ActionListener()
                     {
                        public void actionPerformed(ActionEvent ae)
                        {
                           InputStream is = null;
                           try
                           {
                              URL url = new URL(txtURL.getText());
                              is = url.openStream();
                              StringBuilder sb = new StringBuilder();
                              int b;
                              while ((b = is.read()) != -1)
                                 sb.append((char) b);
                              txtHTML.setText(sb.toString());
                           }
                           catch (IOException ioe)
                           {
                              txtHTML.setText(ioe.getMessage());
                           }
                           finally
                           {
                              txtHTML.setCaretPosition(0);
                              if (is != null)
                                 try
                                 {
                                    is.close();
                                 }
                                 catch (IOException ioe)
                                 {
                                 }
                           }
                        }
                     };
                txtURL.addActionListener(al);
                frame.pack();
                frame.setVisible(true);
             }
          };
      EventQueue.invokeLater(r);
   }
}

Listing 3: ViewPage.java (version 1)

main(String[]) creates a user interface consisting of a textfield for entering an HTML document's URL, and a scrollable textarea for displaying the document's HTML. Pressing the Enter key after entering the URL causes ViewPage to fetch the HTML document and display the results.

Listing 3 is problematic because the document is read on the EDT. The UI becomes unresponsive when it takes a long time to read the document (because of a slow network, for example). The solution to this problem involves using a worker thread (a thread that runs in the background) to read the document. Listing 4 presents one version of this solution.

// ViewPage.java

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.io.InputStream;
import java.io.IOException;

import java.lang.reflect.InvocationTargetException;

import java.net.URL;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class ViewPage
{
   public static void main(String[] args)
   {
      Runnable r;
      r = new Runnable()
          {
             public void run()
             {
                final JFrame frame = new JFrame("View Page");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JPanel panel = new JPanel();
                panel.add(new JLabel("Enter URL"));
                final JTextField txtURL = new JTextField(40);
                panel.add(txtURL);
                frame.getContentPane().add(panel, BorderLayout.NORTH);
                final JTextArea txtHTML = new JTextArea(10, 40);
                frame.getContentPane().add(new JScrollPane (txtHTML),
                                           BorderLayout.CENTER);
                ActionListener al;
                al = new ActionListener()
                     {
                        public void actionPerformed(ActionEvent ae)
                        {
                           txtURL.setEnabled(false);
                           Runnable worker = new Runnable()
                           {
                              public void run()
                              {
                                 InputStream is = null;
                                 try
                                 {
                                    URL url = new URL(txtURL.getText());
                                    is = url.openStream();
                                    final StringBuilder sb;
                                    sb = new StringBuilder();
                                    int b;
                                    while ((b = is.read()) != -1)
                                       sb.append((char) b);
                                    Runnable r = new Runnable()
                                    {
                                       public void run()
                                       {
                                          txtHTML.setText(sb.toString());
                                          txtURL.setEnabled(true);
                                       }
                                    };
                                    try
                                    {
                                       EventQueue.invokeAndWait(r);
                                    }
                                    catch (InterruptedException ie)
                                    {
                                    }
                                    catch (InvocationTargetException ite)
                                    {
                                    }
                                 }
                                 catch (final IOException ioe)
                                 {
                                    Runnable r = new Runnable()
                                    {
                                       public void run()
                                       {
                                          txtHTML.setText(ioe.getMessage());
                                          txtURL.setEnabled(true);
                                       }
                                    };
                                    try
                                    {
                                       EventQueue.invokeAndWait(r);
                                    }
                                    catch (InterruptedException ie)
                                    {
                                    }
                                    catch (InvocationTargetException ite)
                                    {
                                    }
                                 }
                                 finally
                                 {
                                    Runnable r = new Runnable()
                                    {
                                       public void run()
                                       {
                                          txtHTML.setCaretPosition(0);
                                          txtURL.setEnabled(true);
                                       }
                                    };
                                    try
                                    {
                                       EventQueue.invokeAndWait(r);
                                    }
                                    catch (InterruptedException ie)
                                    {
                                    }
                                    catch (InvocationTargetException ite)
                                    {
                                    }
                                    if (is != null)
                                       try
                                       {
                                          is.close();
                                       }
                                       catch (IOException ioe)
                                       {
                                       }
                                 }
                              }
                           };
                           new Thread(worker).start();
                        }
                     };
                txtURL.addActionListener(al);
                frame.pack();
                frame.setVisible(true);
             }
          };
      EventQueue.invokeLater(r);
   }
}

Listing 4: ViewPage.java (version 2)

Listing 4 differs from Listing 3 in that it creates a worker thread to read the HTML document via a runnable. After this document is read, the invokeAndWait(Runnable) method is called to update the UI on the EDT via another runnable. The result is a responsive UI because the EDT is never delayed.

Caution: Never call invokeAndWait(Runnable) from the EDT. Doing so blocks the EDT, which is required to execute the runnable.

Although Listing 4 solves the unresponsive UI problem, the solution is somewhat verbose and non-intuitive. For example, you might wonder why sb isn't declared volatile. This isn't necessary because sb is final, and the construction of its containing class creates a happens-before relationship with any subsequent reads.

Listing 5 presents a more intuitive solution that leverages the abstract javax.swing.SwingWorker class (new in Java SE 6).

// ViewPage.java

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import java.io.InputStream;
import java.io.IOException;

import java.net.URL;

import java.util.concurrent.ExecutionException;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingWorker;

public class ViewPage
{
   public static void main(String[] args)
   {
      Runnable r;
      r = new Runnable()
          {
             public void run()
             {
                final JFrame frame = new JFrame("View Page");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                JPanel panel = new JPanel();
                panel.add(new JLabel("Enter URL"));
                final JTextField txtURL = new JTextField(40);
                panel.add(txtURL);
                frame.getContentPane().add(panel, BorderLayout.NORTH);
                final JTextArea txtHTML = new JTextArea(10, 40);
                frame.getContentPane().add(new JScrollPane (txtHTML),
                                           BorderLayout.CENTER);
                ActionListener al;
                al = new ActionListener()
                     {
                        public void actionPerformed(ActionEvent ae)
                        {
                           txtURL.setEnabled(false);
                           class GetHTML extends SwingWorker<StringBuilder,
                                                             Void>
                           {
                              private String url;
                              GetHTML(String url)
                              {
                                 this.url = url;
                              }
                              @Override
                              public StringBuilder doInBackground()
                              {
                                 StringBuilder sb = new StringBuilder();
                                 InputStream is = null;
                                 try
                                 {
                                    URL url = new URL(this.url);
                                    is = url.openStream();
                                    int b;
                                    while ((b = is.read()) != -1)
                                       sb.append((char) b);
                                    return sb;
                                 }
                                 catch (IOException ioe)
                                 {
                                    sb.setLength(0);
                                    sb.append(ioe.getMessage());
                                    return sb;
                                 }
                                 finally
                                 {
                                    if (is != null)
                                       try
                                       {
                                          is.close();
                                       }
                                       catch (IOException ioe)
                                       {
                                       }
                                 }
                              }
                              @Override
                              public void done()
                              {
                                 try
                                 {
                                    StringBuilder sb = get();
                                    txtHTML.setText(sb.toString());
                                    txtHTML.setCaretPosition(0);
                                 }
                                 catch (ExecutionException ee)
                                 {
                                    txtHTML.setText(ee.getMessage());
                                 }
                                 catch (InterruptedException ie)
                                 {
                                    txtHTML.setText("Interrupted");
                                 }
                                 txtURL.setEnabled(true);
                              }
                           }
                           new GetHTML(txtURL.getText()).execute();
                        }
                     };
                txtURL.addActionListener(al);
                frame.pack();
                frame.setVisible(true);
             }
          };
      EventQueue.invokeLater(r);
   }
}

Listing 5: ViewPage.java (version 3)

This final version of ViewPage relies on GetHTML, a SwingWorker subclass that's declared in the actionPerformed(ActionEvent) method, to read the HTML document on a worker thread (keeping the user interface responsive), and update the user interface with the HTML on the EDT (where Swing code must execute).

When actionPerformed(ActionEvent) is called (the user presses Enter after entering a URL in the textfield), this method instantiates GetHTML with the textfield's text (the textfield is not accessed from the worker thread because Swing is single-threaded), and calls SwingWorker's execute() method.

execute() causes GetHTML's overriding doInBackground() method to be called on a worker thread, which populates a java.lang.StringBuilder object with HTML/error text and returns this object. The EDT then calls the overriding done() method, which accesses the StringBuilder object by calling SwingWorker's get() method, and populates the textarea with these contents.

Note: SwingWorker's get() method throws an instance of the java.util.concurrent.ExecutionException class if an exception is thrown while attempting to retrieve the object returned from doInBackground().

Exercises

  1. What is the event-dispatch thread?
  2. Identify the two rules that you need to follow so that your Swing applications perform correctly.
  3. Why do Listings 4 and 5 first disable the textfield when an action event occurs, and later enable the textfield after the textarea has been populated?

Code

You can download this post's code and answers here. Code was developed and tested with JDK 7u2 on a Windows XP SP3 platform.

* * *

I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).