HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

Thread-Safe GUIs With Swing, by Neal Ford

Remember the old axiom, Be careful what you ask for, you just might get it? That's what happened with the Abstract Windowing Toolkit (AWT), GUI controls, and threading. Developers were tired of always worrying about multithreaded access to GUI elements, so it sounded like a good idea to create an application framework that was always thread-safe.

What do we mean by thread-safe? Two separate threads of execution can access the control at the same time without the developer having to worry about the threads interfering with one another. AWT made this possible...and was consequently very sluggish. The original designers of Java built a lot of thread safety into the language and its libraries. For example, the collections classes from the original JDK (Vector and Hashtable) are always thread-safe. However, that safety comes at a cost. Because there's a great deal of overhead necessary to build thread-safe artifacts, they tend to be much slower than nonthread-safe alternatives. This is true of the collections classes (which is why we now have ArrayList and Hashmap, the nonthread-safe alternatives) and Swing.

When it came time to build JFC and Swing, one of the design decisions was that thread safety would be eschewed in favor of speed. This doesn't mean the controls can never be accessed from multiple threads, but the developer is now responsible for adding code to ensure that no ill effects occur. This article shows how to build thread-safe GUIs in Swing. First, however, I'll show what happens if you don't take care of threading.

Thread Collisions
Consider the application shown in Figure 1. It's a simple list box whose items are updated via a thread. For the first version of this example, no thread safety is built into the code (see Listing 1).

figure 1
Figure 1:  Accessing the contents of the list box

As you can see, the thread updates the contents of the list box continuously. What's bad about this? When it runs, you get the result shown in Figure 1. As the thread updates the list box, the drawing thread in Swing also accesses the elements to keep the GUI representation updated. Because both threads can access the content at the same time, the worker thread can pull items off the content at the same time the drawing thread is accessing them for display. This causes the cascading series of exceptions you see in the figure.

How can this be prevented? By using one of the static methods in the EventQueue class - invokeLater() or invokeAndWait(). These methods were originally in the SwingUtilities class (in JDK 1.1.x) and are now accessible from either class. These methods can take a thread as their parameter and are responsible for executing the thread that's passed to them in sync with the main Swing thread. These two methods determine how you want the update to occur. The invokeLater() method returns immediately, placing the update code in the regular event queue of Swing. This means the updating will take place as soon as possible, but it doesn't make your code wait for the update to occur. There may be situations in which you want your code to wait until the update has occurred. The invokeAndWait() method won't return until the update is complete. These methods are the secret to handling graceful threaded Swing code.

Listing 2 demonstrates how to solve the original problem. I've added code to call invokeLater() to the code that must update the Swing control's contents. Now when the application runs, no exceptions occur because both the update and drawing threads are no longer in conflict. The pertinent change to the code appears in the body of the run() method of the WorkerThread class and is shown here:

EventQueue.invokeLater(new Runnable() {
public void run() {
if (model.contains(i))
model.removeElement(i);
else
model.addElement(i);
}
});
This is a common technique for updating Swing controls from within a thread. A new anonymous class that implements the Runnable interface is created. The code in the run() method is the code placed in the main event thread in Swing. Notice that this is the same code used in the previous example to update the list box's model - the difference here is the call to EventQueue.invokeLater().

Progress for Long-Running Processes
Here's a practical example of the need to have a thread update in real time. A common chore in applications is to show the users the progress of some long-running process. There are two ways to create such a process. If coded directly into the application (i.e., not in a thread), the application can't show progress because the process hasn't finished yet. In other words, if you do the work in an event handler, you're occupying the main event thread. If the process was spawned in a thread, the thread safety of the GUI must be considered. The second solution is the only real choice if you want to provide feedback, and armed with the EventQueue methods listed above, it's easy to accommodate.

For this sample application I copy a collection of files from one location to another. This is a classic example of a process for which you want to provide feedback. This application copies all the source files from the Java SwingSet2 demo to the temp directory (hey, I had to copy something from somewhere to somewhere else, didn't I?). As you can see in Figure 2, the user interface provides feedback on the percentage of completion for the two tasks. First, the application deletes the files from the target directory (if they've been previously copied there) and then copies the source files to the target.

figure 2
Figure 2:  Thread-safe user interfaces

Because there are two threads with common traits at work in this application, I first declared an abstract thread class to encapsulate the similarities. This class appears in Listing 3. Two characteristics are required from each thread. First, they must have a reference to the frame class to access the UI widgets to notify them of the progress. Second, a terminate request flag will appear in this base class. As you probably know, the thread stop() method has been deprecated in Java 2 because it can lead to undesirable situations. Now, to be able to stop a thread, you must provide a suicide watch flag - not so much a command to stop as a request that the thread commit suicide. Both worker threads subclass the abstract WorkerThread class.

The delete thread appears in Listing 4. One item to note in both threads is the manner in which they interact with the frame class. The thread doesn't directly access the JProgressBar on the frame. Instead, frame methods are called to handle the actual initialization and updating. The frame's initCopyProgress() method is shown here:

void initCopyProgress(int numFiles) {
jprgrsbrCopy.setMaximum(numFiles);
jprgrsbrCopy.setMinimum(0);
}
The updateCopyProgress() method handles the updating of the progress bar's progress.
void updateCopyProgress() {
jprgrsbrCopy.setValue(
jprgrsbrCopy.getValue() + 1);
}

Making the frame's methods update the progress bar is a good idea so the thread doesn't have too much knowledge of how the user interface is handling the progress mechanism. The UI could now change to incorporate a gauge or some other progress technique without affecting the threads. The remainder of the DeleteThread deletes the files in the target directory. Notice the call to EventQueue.invokeLater() to update the frame. The last act of the DeleteThread is to instantiate and call the CopyThread (see Listing 5). Both threads could have been created at the same time and the CopyThread made to wait on the DeleteThread. However, because these two operations must run serially, it makes sense to spawn one from the other.

The CopyThread performs many of the same housekeeping operations as the DeleteThread. Most of the code in the CopyThread concerns itself with copying files, which is not relevant to this discussion. For our purposes note the call to update the user interface in a call to EventQueue. invokeLater().

Building user interfaces that gracefully spawn threads and in turn report progress along the way is easy once you understand the requirements built into the Swing application framework. Now we have a rich user interface library that offers better speed and capabilities than the old one. When necessary, the developer can make it thread-safe to give it the capabilities of the old AWT framework without the disadvantages.

Author Bio
Neal Ford, vice president of technology at the DSW Group, is also the designer and developer of applications, instructional materials, magazine articles, and video presentations. He's written two books, Developing with Delphi: Object-Oriented Techniques and JBuilder 3 Unleashed. He can be contacted at [email protected]

	


Listing 1
 
import java.awt.*; 
import java.awt.event.*; 
import java.util.*; 
import javax.swing.*; 

public class BadThread { 
    public static void main(String[] args) { 
        JFrame frame = new TestFrame(); 
        frame.show(); 
    } 
} 

class TestFrame extends JFrame { 
    public TestFrame() { 
        setTitle("Bad Thread Example"); 
        setSize(400,300); 
        setDefaultCloseOperation( 
                JFrame.EXIT_ON_CLOSE); 
        model = new DefaultListModel(); 

        JList list = new JList(model); 
        JScrollPane scrollPane = 
                new JScrollPane(list); 

        JPanel p = new JPanel(); 
        p.add(scrollPane); 
        getContentPane().add(p, "Center"); 

        JButton b = new JButton("Fill List"); 
        b.addActionListener(new ActionListener() { 
            public void actionPerformed( 
                    ActionEvent event) { 
                new WorkerThread(model).start(); 
            } 
        }); 
        p = new JPanel(); 
        p.add(b); 
        getContentPane().add(p, "North"); 
    } 

    private DefaultListModel model; 
} 

class WorkerThread extends Thread { 
    public WorkerThread(DefaultListModel aModel) { 
        model = aModel; 
        generator = new Random(); 
    } 

    public void run() { 
        while (true) { 
            Integer i = 
                new Integer(generator.nextInt(10)); 

            if (model.contains(i)) 
                model.removeElement(i); 
            else 
                model.addElement(i); 

            yield(); 
        } 
    } 

    private DefaultListModel model; 
    private Random generator; 
} 

Listing 2
 
import java.awt.*; 
import java.awt.event.*; 
import java.util.*; 
import javax.swing.*; 

public class GoodThread { 
    public static void main(String[] args) { 
        JFrame frame = new TestFrame(); 
        frame.show(); 
    } 
} 

class TestFrame extends JFrame { 
    public TestFrame() { 
        setTitle("Good Thread Example"); 
        setSize(400,300); 
        setDefaultCloseOperation( 
                JFrame.EXIT_ON_CLOSE); 
        model = new DefaultListModel(); 

        JList list = new JList(model); 
        JScrollPane scrollPane = 
                new JScrollPane(list); 

        JPanel p = new JPanel(); 
        p.add(scrollPane); 
        getContentPane().add(p, "Center"); 

        JButton b = new JButton("Fill List"); 
        b.addActionListener(new ActionListener() { 
            public void actionPerformed( 
                    ActionEvent event) { 
                new WorkerThread(model).start(); 
            } 
        }); 
        p = new JPanel(); 
        p.add(b); 
        getContentPane().add(p, "North"); 
    } 

    private DefaultListModel model; 
} 

class WorkerThread extends Thread { 
    public WorkerThread(DefaultListModel aModel) { 
        model = aModel; 
        generator = new Random(); 
    } 

    public void run() { 
        while (true) { 
            final Integer i = 
                new Integer(generator.nextInt(10)); 
            EventQueue.invokeLater(new Runnable() { 
                public void run() { 
                    if (model.contains(i)) 
                        model.removeElement(i); 
                    else 
                        model.addElement(i); 
                } 
            }); 
            yield(); 
        } 
    } 

    private DefaultListModel model; 
    private Random generator; 
} 
  
  
  
  
  
  

Listing 3
 
abstract class WorkerThread extends Thread { 
  protected SwingGuiFrame ui; 
  private boolean terminateRequested = false; 

  public WorkerThread(SwingGuiFrame ui) { 
    this.ui = ui; 
  } 

  public void terminate() { 
    terminateRequested = true; 
  } 

  public boolean isTerminateRequestd() { 
    return terminateRequested; 
  } 
  

Listing 4
 
class DeleteThread extends WorkerThread { 
  private static final String PREFIX = 
      "e:/temp/test"; 

  DeleteThread(SwingGuiFrame ui) { 
    super(ui); 
  } 

  public void run() { 
    File a = new File(PREFIX); 
    String fileList[] = a.list(); 
    ui.initDeleteProgress(fileList.length); 
    int numFiles = fileList.length; 
    for (int i = 0; i < fileList.length; i++) { 
      File f = new File(PREFIX + '/' + 
          fileList[i]); 
      if (f.delete()) { 
        System.out.println("Deleted file" + 
            fileList[i]); 
      } 
      EventQueue.invokeLater(new Runnable() { 
        public void run() { 
          ui.updateDeleteProgress(); 
        } 
      }); 
      if (isTerminateRequestd()) { 
        return; 
      } 
      yield(); 
    } //for 
    ui.copyThread = new CopyThread(ui); 
    ui.copyThread.start(); 
  } 
} 
  

Listing 5
 
class CopyThread extends WorkerThread { 
  String filesToCopy[]; 
  private static final String PATH = 
      "d:/jdk1.3/demo/jfc/SwingSet2/src/"; 
  private static final String OUTPUT_PATH = 
      "e:/temp/test/"; 

  CopyThread(SwingGuiFrame ui) { 
    super(ui); 
    File srcDir = new File(PATH); 
    filesToCopy = 
        new String[srcDir.list().length]; 
    filesToCopy = srcDir.list(); 
    ui.initCopyProgress(filesToCopy.length); 
  } 

  public void run() { 
    int numFiles = filesToCopy.length; 
    for (int i = 0; i < numFiles; i++) { 
      System.out.println("Copying file:" + 
          filesToCopy[i]); 
      copyFile(filesToCopy[i]); 
      EventQueue.invokeLater(new Runnable() { 
        public void run() { 
          ui.updateCopyProgress(); 
        } 
      }); 
      if (isTerminateRequestd()) { 
        return; 
      } 
      yield(); 
    } 
  } 

  private void copyFile(String fileToCopy) { 
    BufferedInputStream bis = null; 
    BufferedOutputStream bos = null; 
    try { 
      File in = new File(PATH + fileToCopy); 
      File out = new File( 
          OUTPUT_PATH + fileToCopy); 
      FileInputStream fis = 
          new FileInputStream(in); 
      bis = new BufferedInputStream(fis, 256); 
      FileOutputStream fos = 
          new FileOutputStream(out); 
      bos = new BufferedOutputStream(fos); 
      while (bis.available() > 0) { 
        bos.write(bis.read()); 
      } 
      bos.flush(); 
    } catch (IOException ioe) { 
      System.out.println(ioe.getMessage()); 
      JOptionPane.showMessageDialog( 
          new Frame(), ioe.getMessage(), 
          "File Error", 
          JOptionPane.ERROR_MESSAGE); 
    } finally { 
      try { 
        bis.close(); 
        bos.close(); 
      } catch (IOException ignored) { 
      } //catch 
    } //finally 
  } 
} 


  
 
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.