Threads and Swing


As we mentioned in the introduction to this chapter, one of the reasons to use threads in your programs is to make your programs more responsive. When your program needs to do something time consuming, then you should fire up another worker thread instead of blocking the user interface.

However, you have to be careful what you do in a worker thread because, perhaps surprisingly, Swing is not thread safe. If you try to manipulate user interface elements from multiple threads, then your user interface can become corrupted.

To see the problem, run the test program in Example 1-10. When you click the Bad button, a new thread is started whose run method tortures a combo box, randomly adding and removing values.

 public void run() {    try    {       while (true)       {          int i = Math.abs(generator.nextInt());          if (i % 2 == 0)             combo.insertItemAt(new Integer(i), 0);          else if (combo.getItemCount() > 0)             combo.removeItemAt(i % combo.getItemCount());          sleep(1);       }       catch (InterruptedException e) {}    } } 

Try it out. Click the Bad button. Click the combo box a few times. Move the scroll bar. Move the window. Click the Bad button again. Keep clicking the combo box. Eventually, you should see an exception report (see Figure 1-8).

Figure 1-8. Exception reports in the console


What is going on? When an element is inserted into the combo box, the combo box fires an event to update the display. Then, the display code springs into action, reading the current size of the combo box and preparing to display the values. But the worker thread keeps goingoccasionally resulting in a reduction of the count of the values in the combo box. The display code then thinks that there are more values in the model than there actually are, asks for nonexistent values, and triggers an ArrayIndexOutOfBounds exception.

This situation could have been avoided by enabling programmers to lock the combo box object while displaying it. However, the designers of Swing decided not to expend any effort to make Swing thread safe, for two reasons. First, synchronization takes time, and nobody wanted to slow down Swing any further. More important, the Swing team checked out the experience other teams had with thread-safe user interface toolkits. What they found was not encouraging. Builders of a user interface toolkit want it to be extensible so that other programmers can add their own user interface components. However, user interface programmers using thread-safe toolkits turned out to be confused by the demands for synchronization and tended to create components that were prone to deadlocks.

The "Single Thread" Rule

When you use threads together with Swing, you have to follow a few simple rules. First, however, let's see what threads are present in a Swing program.

Every Java application starts with a main method that runs in the main thread. In a Swing program, the main method typically does the following:

  • First it calls a constructor that lays out components in a frame window;

  • Then it invokes the setVisible method on the frame window.

When the first window is shown, a second thread is created, the event dispatch thread. All event notifications, such as calls to actionPerformed or paintComponent, run in the event dispatch thread. The main thread keeps running until the main method exits. Usually, of course, the main method exits immediately after displaying the frame window.

Other threads, such as the thread that posts events into the event queue, are running behind the scenes, but those threads are invisible to the application programmer.

In a Swing application, essentially all code is contained in event handlers to respond to user interface and repaint requests. All that code runs on the event dispatch thread. Here are the rules that you need to follow.

  1. If an action takes a long time, fire up a new thread to do the work. If you take a long time in the event dispatch thread, the application seems "dead" because it cannot respond to any events.

  2. If an action can block on input or output, fire up a new thread to do the work. You don't want to freeze the user interface for the potentially indefinite time that a network connection is unresponsive.

  3. If you need to wait for a specific amount of time, don't sleep in the event dispatch thread. Instead, use timer events.

  4. The work that you do in your threads cannot touch the user interface. Read any information from the UI before you launch your threads, launch them, and then update the user interface from the event dispatching thread once the threads have completed.

The last rule is often called the single-thread rule for Swing programming. There are a few exceptions to the single-thread rule.

  1. A few Swing methods are thread safe. They are specially marked in the API documentation with the sentence "This method is thread safe, although most Swing methods are not." The most useful among these thread-safe methods are

     JTextComponent.setText JTextArea.insert JTextArea.append JTextArea.replaceRange 

  2. The following methods of the JComponent class can be called from any thread:

     repaint revalidate 

    The repaint method schedules a repaint event. You use the revalidate method if the contents of a component have changed and the size and position of the component must be updated. The revalidate method marks the component's layout as invalid and schedules a layout event. (Just like paint events, layout events are coalesced. If multiple layout events are in the event queue, the layout is only recomputed once.)

    NOTE

    We used the repaint method many times in Volume 1 of this book, but the revalidate method is less common. Its purpose is to force a layout of a component after the contents have changed. The traditional AWT has a validate method to force the layout of a component. For Swing components, you should simply call revalidate instead. (However, to force the layout of a JFrame, you still need to call validatea JFrame is a Component but not a JComponent.)


  3. You can safely add and remove event listeners in any thread. Of course, the listener methods will be invoked in the event dispatching thread.

  4. You can construct components, set their properties, and add them into containers, as long as none of the components have been realized. A component has been realized if it can receive paint or validation events. This is the case as soon as the setVisible(true) or pack methods have been invoked on the component, or if the component has been added to a container that has been realized. Once a component has been realized, you can no longer manipulate it from another thread.

    In particular, you can create the GUI of an application in the main method before calling setVisible(true), and you can create the GUI of an applet in the applet constructor or the init method.

Now suppose you fire up a separate thread to run a time-consuming task. You may want to update the user interface to indicate progress while your thread is working. When your task is finished, you want to update the GUI again. But you can't touch Swing components from your thread. For example, if you want to update a progress bar or a label text, then you can't simply set its value from your thread.

To solve this problem, you can use two convenient utility methods in any thread to add arbitrary actions to the event queue. For example, suppose you want to periodically update a label in a thread to indicate progress. You can't call label.setText from your thread. Instead, use the invokeLater and invokeAndWait methods of the EventQueue class to have that call executed in the event dispatching thread.

Here is what you do. You place the Swing code into the run method of a class that implements the Runnable interface. Then, you create an object of that class and pass it to the static invokeLater or invokeAndWait method. For example, here is how to update a label text.

 EventQueue.invokeLater(new    Runnable()    {       public void run()       {          label.setText(percentage + "% complete");       }    }); 

The invokeLater method returns immediately when the event is posted to the event queue. The run method is executed asynchronously. The invokeAndWait method waits until the run method has actually been executed.

In the situation of updating a progress label, the invokeLater method is more appropriate. Users would rather have the worker thread make more progress than have the most precise progress indicator.

Both methods execute the run method in the event dispatch thread. No new thread is created.

Example 1-10 demonstrates how to use the invokeLater method to safely modify the contents of a combo box. If you click on the Good button, a thread inserts and removes numbers. However, the actual modification takes place in the event dispatching thread.

Example 1-10. SwingThreadTest.java
   1. import java.awt.*;   2. import java.awt.event.*;   3. import java.util.*;   4. import javax.swing.*;   5.   6. /**   7.    This program demonstrates that a thread that   8.    runs in parallel with the event dispatch thread   9.    can cause errors in Swing components.  10. */  11. public class SwingThreadTest  12. {  13.    public static void main(String[] args)  14.    {  15.       SwingThreadFrame frame = new SwingThreadFrame();  16.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  17.       frame.setVisible(true);  18.    }  19. }  20.  21. /**  22.    This frame has two buttons to fill a combo box from a  23.    separate thread. The "Good" button uses the event queue,  24.    the "Bad" button modifies the combo box directly.  25. */  26. class SwingThreadFrame extends JFrame  27. {  28.    public SwingThreadFrame()  29.    {  30.       setTitle("SwingThreadTest");  31.  32.       final JComboBox combo = new JComboBox();  33.       combo.insertItemAt(new Integer(Integer.MAX_VALUE), 0);  34.       combo.setPrototypeDisplayValue(combo.getItemAt(0));  35.       combo.setSelectedIndex(0);  36.  37.       JPanel panel = new JPanel();  38.  39.       JButton goodButton = new JButton("Good");  40.       goodButton.addActionListener(new ActionListener()  41.          {  42.             public void actionPerformed(ActionEvent event)  43.             {  44.                new Thread(new GoodWorkerRunnable(combo)).start();  45.             }  46.          });  47.       panel.add(goodButton);  48.       JButton badButton = new JButton("Bad");  49.       badButton.addActionListener(new ActionListener()  50.          {  51.             public void actionPerformed(ActionEvent event)  52.             {  53.                new Thread(new BadWorkerRunnable(combo)).start();  54.             }  55.          });  56.       panel.add(badButton);  57.       panel.add(combo);  58.       add(panel);  59.       pack();  60.    }  61. }  62.  63. /**  64.    This runnable modifies a combo box by randomly adding  65.    and removing numbers. This can result in errors because  66.    the combo box methods are not synchronized and both the worker  67.    thread and the event dispatch thread access the combo box.  68. */  69. class BadWorkerRunnable implements Runnable  70. {  71.    public BadWorkerRunnable(JComboBox aCombo)  72.    {  73.       combo = aCombo;  74.       generator = new Random();  75.    }  76.  77.    public void run()  78.    {  79.       try  80.       {  81.          while (true)  82.          {  83.             combo.showPopup();  84.             int i = Math.abs(generator.nextInt());  85.             if (i % 2 == 0)  86.                combo.insertItemAt(new Integer(i), 0);  87.             else if (combo.getItemCount() > 0)  88.                combo.removeItemAt(i % combo.getItemCount());  89.             Thread.sleep(1);  90.          }  91.       }  92.       catch (InterruptedException e) {}  93.    }  94.  95.    private JComboBox combo;  96.    private Random generator;  97. }  98.  99. /** 100.    This runnable modifies a combo box by randomly adding 101.    and removing numbers. In order to ensure that the 102.    combo box is not corrupted, the editing operations are 103.    forwarded to the event dispatch thread. 104. */ 105. class GoodWorkerRunnable implements Runnable 106. { 107.    public GoodWorkerRunnable(JComboBox aCombo) 108.    { 109.       combo = aCombo; 110.       generator = new Random(); 111.    } 112. 113.    public void run() 114.    { 115.       try 116.       { 117.          while (true) 118.          { 119.             EventQueue.invokeLater(new 120.                Runnable() 121.                { 122.                   public void run() 123.                   { 124.                      combo.showPopup(); 125.                      int i = Math.abs(generator.nextInt()); 126.                      if (i % 2 == 0) 127.                         combo.insertItemAt(new Integer(i), 0); 128.                      else if (combo.getItemCount() > 0) 129.                         combo.removeItemAt(i % combo.getItemCount()); 130.                   } 131.                }); 132.             Thread.sleep(1); 133.          } 134.       } 135.       catch (InterruptedException e) {} 136.    } 137. 138.    private JComboBox combo; 139.    private Random generator; 140. } 


 java.awt.EventQueue 1.0 

  • static void invokeLater(Runnable runnable) 1.2

    causes the run method of the runnable object to be executed in the event dispatch thread after pending events have been processed.

  • static void invokeAndWait(Runnable runnable) 1.2

    causes the run method of the runnable object to be executed in the event dispatch thread after pending events have been processed. This call blocks until the run method has terminated.

A Swing Worker

When a user issues a command for which processing takes a long time, you will want to fire up a new thread to do the work. As you saw in the preceding section, that thread should use the EventQueue.invokeLater method to update the user interface.

Several authors have produced convenience classes to ease this task. A well-known example is Hans Muller's SwingWorker class, described in http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html. Here, we present a slightly different class that makes it easier for a thread to update the user interface after each unit of work.

The program in Example 1-11 has commands for loading a text file and for canceling the file loading process. You should try the program with a long file, such as the full text of The Count of Monte Cristo, supplied in the gutenberg directory of the book's companion code. The file is loaded in a separate thread. While the file is read, the Open menu item is disabled and the Cancel item is enabled (see Figure 1-9). After each line is read, a line counter in the status bar is updated. After the reading process is complete, the Open menu item is reenabled, the Cancel item is disabled, and the status line text is set to Done.

Figure 1-9. Loading a file in a separate thread


This example shows the typical UI activities of a worker thread:

  • Make an initial update to the UI before starting the work.

  • After each work unit, update the UI to show progress.

  • After the work is finished, make a final change to the UI.

The SwingWorkerTask class in Example 1-11 makes it easy to implement such a task. You extend the class and override the init, update, and finish methods and implement the logic for the UI updates. The superclass contains convenience methods doInit, doUpdate, and doFinish that supply the unwieldy code for running these methods in the event dispatch thread. For example,

 private void doInit() {    EventQueue.invokeLater(new       Runnable()       {          public void run() { init(); }       }); } 

Then supply a work method that contains the work of the task. In the work method, you need to call doUpdate (not update) after every unit of work. For example, the file reading task has this work method:

 public void work() // exception handling not shown {    Scanner in = new Scanner(new FileInputStream(file));    textArea.setText("");    while (!Thread.currentThread().isInterrupted() && in.hasNextLine())    {       lineNumber++;       line = in.nextLine();       textArea.append(line);       textArea.append("\n");       doUpdate();    } } 

The SwingWorkerTask class implements the Runnable interface. The run method is straightforward:

 public final void run() {    doInit();    try    {       done = false;       work();    }    catch (InterruptedException e)    {    }    finally    {       done = true;       doFinish();    } } 

You simply start a thread with an object of your SwingWorkerTask object or submit it to an Executor. The sample program does that in the handler for the Open menu item. The handler returns immediately, allowing the user to select other user interface elements.

This simple technique allows you to execute time-consuming tasks while keeping the user interface responsive.

Example 1-11. SwingWorkerTest.java

[View full width]

   1. import java.awt.*;   2. import java.awt.event.*;   3. import java.io.*;   4. import java.util.*;   5. import java.util.concurrent.*;   6. import javax.swing.*;   7.   8. /**   9.    This program demonstrates a worker thread that runs  10.    a potentially time-consuming task.  11. */  12. public class SwingWorkerTest  13. {  14.    public static void main(String[] args) throws Exception  15.    {  16.       JFrame frame = new SwingWorkerFrame();  17.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  18.       frame.setVisible(true);  19.    }  20. }  21.  22. /**  23.    This frame has a text area to show the contents of a text file,  24.    a menu to open a file and cancel the opening process, and  25.    a status line to show the file loading progress.  26. */  27. class SwingWorkerFrame extends JFrame  28. {  29.    public SwingWorkerFrame()  30.    {  31.       chooser = new JFileChooser();  32.       chooser.setCurrentDirectory(new File("."));  33.  34.       textArea = new JTextArea();  35.       add(new JScrollPane(textArea));  36.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);  37.  38.       statusLine = new JLabel();  39.       add(statusLine, BorderLayout.SOUTH);  40.  41.       JMenuBar menuBar = new JMenuBar();  42.       setJMenuBar(menuBar);  43.  44.       JMenu menu = new JMenu("File");  45.       menuBar.add(menu);  46.  47.       openItem = new JMenuItem("Open");  48.       menu.add(openItem);  49.       openItem.addActionListener(new  50.          ActionListener()  51.          {  52.             public void actionPerformed(ActionEvent event)  53.             {  54.                // show file chooser dialog  55.                int result = chooser.showOpenDialog(null);  56.  57.                // if file selected, set it as icon of the label  58.                if (result == JFileChooser.APPROVE_OPTION)  59.                {  60.                   readFile(chooser.getSelectedFile());  61.                }  62.             }  63.          });  64.  65.       cancelItem = new JMenuItem("Cancel");  66.       menu.add(cancelItem);  67.       cancelItem.setEnabled(false);  68.       cancelItem.addActionListener(new  69.          ActionListener()  70.          {  71.             public void actionPerformed(ActionEvent event)  72.             {  73.                if (workerThread != null) workerThread.interrupt();  74.             }  75.          });  76.    }  77.  78.    /**  79.       Reads a file asynchronously, updating the UI during the reading process.  80.       @param file the file to read  81.    */  82.    public void readFile(final File file)  83.    {  84.       Runnable task = new  85.          SwingWorkerTask()  86.          {  87.             public void init()  88.             {  89.                lineNumber = 0;  90.                openItem.setEnabled(false);  91.                cancelItem.setEnabled(true);  92.             }  93.  94.             public void update()  95.             {  96.                statusLine.setText("" + lineNumber);  97.             }  98.  99.             public void finish() 100.             { 101.                workerThread = null; 102.                openItem.setEnabled(true); 103.                cancelItem.setEnabled(false); 104.                statusLine.setText("Done"); 105.             } 106. 107.             public void work() 108.             { 109.                try 110.                { 111.                   Scanner in = new Scanner(new FileInputStream(file)); 112.                   textArea.setText(""); 113.                   while (!Thread.currentThread().isInterrupted() && in.hasNextLine()) 114.                   { 115.                      lineNumber++; 116.                      line = in.nextLine(); 117.                      textArea.append(line); 118.                      textArea.append("\n"); 119.                      doUpdate(); 120.                   } 121.                } 122.                catch (IOException e) 123.                { 124.                   JOptionPane.showMessageDialog(null, "" + e); 125.                } 126.             } 127. 128.             private String line; 129.             private int lineNumber; 130.          }; 131. 132.       workerThread = new Thread(task); 133.       workerThread.start(); 134.    } 135. 136.    private JFileChooser chooser; 137.    private JTextArea textArea; 138.    private JLabel statusLine; 139.    private JMenuItem openItem; 140.    private JMenuItem cancelItem; 141.    private Thread workerThread; 142. 143.    public static final int DEFAULT_WIDTH = 450; 144.    public static final int DEFAULT_HEIGHT = 350; 145. } 146. 147. /** 148.    Extend this class to define an asynchronous task 149.    that updates a Swing UI. 150. */ 151. abstract class SwingWorkerTask implements Runnable 152. { 153.    /** 154.       Place your task in this method. Be sure to call doUpdate(), not update(), to  show the 155.       update after each unit of work. 156.    */ 157.    public abstract void work() throws InterruptedException; 158. 159.    /** 160.       Override this method for UI operations before work commences. 161.    */ 162.    public void init() {} 163.    /** 164.       Override this method for UI operations after each unit of work. 165.    */ 166.    public void update() {} 167.    /** 168.       Override this method for UI operations after work is completed. 169.    */ 170.    public void finish() {} 171. 172.    private void doInit() 173.    { 174.       EventQueue.invokeLater(new 175.          Runnable() 176.          { 177.             public void run() { init(); } 178.          }); 179.    } 180. 181.    /** 182.       Call this method from work() to show the update after each unit of work. 183.    */ 184.    protected final void doUpdate() 185.    { 186.       if (done) return; 187.       EventQueue.invokeLater(new 188.          Runnable() 189.          { 190.             public void run() { update(); } 191.          }); 192.    } 193. 194.    private void doFinish() 195.    { 196.       EventQueue.invokeLater(new 197.          Runnable() 198.          { 199.             public void run() { finish(); } 200.          }); 201.    } 202. 203.    public final void run() 204.    { 205.       doInit(); 206.       try 207.       { 208.          done = false; 209.          work(); 210.       } 211.       catch (InterruptedException ex) 212.       { 213.       } 214.       finally 215.       { 216.          done = true; 217.          doFinish(); 218.       } 219.    } 220. 221.    private boolean done; 222. } 



    Core JavaT 2 Volume II - Advanced Features
    Building an On Demand Computing Environment with IBM: How to Optimize Your Current Infrastructure for Today and Tomorrow (MaxFacts Guidebook series)
    ISBN: 193164411X
    EAN: 2147483647
    Year: 2003
    Pages: 156
    Authors: Jim Hoskins

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net