SWING WORKER EXAMPLE

Java provides a neat way to carry out long lasting jobs without have to worry about the complexity of threads or lack of responsiveness in an application (by application we mean Swing applications). It is called SwingWorker (Java Doc). It is not the latest thing on Earth (released with Java 1.6) and you may have already read about it. In this article we will see how to use it and why it was created to start with.

All code listed below is available at: https://java-creed-examples.googlecode.com/svn/swing/Swing Worker Example/. Most of the examples will not contain the whole code and may omit fragments which are not relevant to the example being discussed. The readers can download or view all code from the above link.

Why do we need the SwingWorker?

Say we need to create an application that performs some task that take a considerable amount of time to complete. Downloading a large file or executing a complex database query are good examples of such tasks. Let us assume that these tasks are triggered by the users using a button. With single threaded applications, the user clicks the button that starts the process and then has to wait for the task to finish before the user can do something else with the application.

Single Thread

Single Thread

The application will become unresponsive as the only thread available is carrying the long task. The user cannot even cancel the task halfway through neither, as the application is busy carrying the long task. The application will become responsive only when the long task is finished. Unfortunately many applications manifest such behaviour and users get frustrated as there is no way to cancel such task or interact with the application before this long task is complete.

Multithreading address this problem. It enables the application to have the long tasks executed on a different thread. The application handles small tasks, such as button clicks, by one thread and the long taking tasks by another thread.

Two Threads

Two Threads

This figure shows only two threads, but an application can have more than just two threads. One can say that threads solved this problem and this is true. But threads create new challenges. The figure above shows two threads that work independently, that is, one thread does not communicate or exchange data with the other thread. This may not always be the case and threads may need to share data.

Swing, like many other GUI frameworks, makes use of what is called thread confinement. What is this? All Swing objects are handled by one thread only, the event dispatcher thread. We just agreed that multithreading yields more responsive applications, then why Swing uses one thread? After several attempts (not only in Java but in many other languages too such as C++) in making GUI components multithreaded, it was decided to handle all GUI objects (such as buttons, lists, models and the like) by one thread. All multithreaded GUI prototypes suffer from deadlocks due to reasons which are beyond the scope of this article. For more information about this, please refer to this article.

Therefore all Swing objects must be accessed only from the event dispatcher thread.

This leads back to the original problem. We cannot share Swing objects with other threads outside the event dispatcher thread. This is why we start a Swing application as shown below.

package com.javacreed.examples.swing.worker.part1;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class Main {
  public static void main(final String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        final JFrame frame = new JFrame();
        frame.setTitle("Test Frame");
        frame.setSize(600, 400);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
      }
    });
  }
}

Here we are instructing Java to execute our Runnable (Java Doc) by the event dispatcher thread. Therefore the JFrame (Java Doc) exists only within this thread. If all events are handled by one thread, then that thread will be block when we add long lasting tasks to the queue. The SwingWorker offers a neat solution to this problem as we will see in this article.

SwingWorker

SwingWorker is an abstract class which hides the threading complexities from the developer. It is an excellent candidate for applications that are required to execute tasks (such as retrieving information over the network/Internet or other slow sources) which may take some time to finish. It is ideal to detach such tasks from the application and simply keep an eye on their progress and provide means for cancelling them.

The following example illustrates a simple empty worker that will return/evaluate to an integer when the given task is finished. It will inform the application with what’s happening using objects of type string, basically text messages.

package com.javacreed.examples.swing.worker.part2;

import java.util.List;

import javax.swing.SwingWorker;

public class MyBlankWorker extends SwingWorker<Integer, String> {

  @Override
  protected Integer doInBackground() throws Exception {
    // Start
    publish("Start");
    setProgress(1);
    
    // More work was done
    publish("More work was done");
    setProgress(10);

    // Complete
    publish("Complete");
    setProgress(100);
    return 1;
  }
  
  @Override
  protected void process(List<String> chunks) {
    // Messages received from the doInBackground() (when invoking the publish() method)
  }
}

Let’s understand the anatomy of the SwingWorker class.

  • The SwingWorker class provides two placeholders (generics). The first one represents the type of object returned when the worker has finished working. The second one represents the type of information that the worker will use to inform (update) the application with its progress, and is highlighted in the following example.
    public class MyBlankWorker extends SwingWorker<Integer, String> {
    
      @Override
      protected Integer doInBackground() throws Exception {
        // Start
        publish("Start");
        setProgress(1);
        
        // More work was done
        publish("More work was done");
        setProgress(10);
    
        // Complete
        publish("Complete");
        setProgress(100);
        return 1;
      }
      
      @Override
      protected void process(List< String> chunks) {
        // Messages received from the doInBackground() (when invoking the publish() method)
      }
    }
    
  • The SwingWorker class also provides means to update the progress by means of an Integer which has nothing to do with the two generics mentioned before. This is managed through the setProgress() method which take an integer between 0 and 100 both inclusive.
  • The method doInBackground() is where the complex and long task is executed. This method is not invoked by the event dispatcher thread, but by another thread (referred to hereunder as the worker thread). From this method we can update the progress using either the publish()method and/or the setProgress(). Here something very important happens. The invocations made by these two methods will add small tasks to the event dispatcher thread creating a one-way bridge between the thread that is doing the work and the event dispatcher thread.

    Observation

    The doInBackground() may invoke the publish() method and/or the setProgress() too often and may flood the event dispatcher thread queue. In order to mitigate this problem, the small tasks may be suppressed or coalesced. Therefore several requests may result in to one small task in the event dispatcher thread queue.

  • The method publish() is invoked several times from within the doInBackground() method and works together with the process(). Further to what was described above, the publish() method is invoked from the worker thread, while the process() is invoked by the event dispatcher thread. The following image illustrates this.

    Relation between the publish and process methods

    Relation between the publish and process methods

    The long grey bar represents the doInBackground() method while the small red bars within it, represents the invocation of thepublish() method. As shown above, these requests may be coalesced into one request on the even dispatcher thread. The two green bars represent the number of times the process() method is invoked. In this example, the publish() method is invoked a total of 12 times, whereas the process() method is invoked only twice. This also provides a performance boost as the event dispatcher thread is nor overloaded with too many small requests.

    Furthermore, the process() method is invoked asynchronously on the event dispatch thread. The above image shows this clearly and we have no control to when this is actually invoked. It depends on the amount of work that the event dispatcher thread has.

    For example:

     publish("a");
     publish("b", "c");
     publish("d", "e", "f");
    

    may result in:

     process("a", "b", "c", "d", "e", "f")
    

    Observation

    Since the process() method is invoked on the event dispatcher thread, we can access and modify the state of Swing components without having to worry about compromising the thread-safety. We do not have to worry about thread-safety when using the Swing Worker as this has been dealt with by the same SwingWorker on our behalf.

  • The setProgress() works very similar to what was described above, with one difference. The application needs to register a listener in order to receive progress notifications. This will be explored in more detail later on in this article.

Here we have described the functionality of each major block and the role these blocks play. Now we will see a practical example of how we can utilise the SwingWorker within an application.

SwingWorker Example

Let say, for example, we need to find the number of occurrences of a given word (or phrase) with in some text documents located under a directory as shown in the following application.

Search Word Application

Search Word Application

This application allows the user to provide the word to search and the path where to look for. Once the user clicks the search button, the Swing worker starts searching in the background. Thus it frees the event thread avoiding the application from freezing. While waiting for the results, the user can cancel the task by pressing the cancel button that is shown instead of the search button.

The swing working is a good candidate for such a problem. The files listing and searching is quite a long task and can take a couple of seconds (if not minutes) to complete. If this task is performed by the event dispatcher thread, then this thread will not be able to do anything else until this task is complete. The user will not be able to cancel the task.

The following swing worker performs all the work we need.

package com.javacreed.examples.swing.worker.part3;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JTextArea;
import javax.swing.SwingWorker;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.lang.StringUtils;

/**
 * Searches the text files under the given directory and counts the number of instances a given word is found
 * in these file.
 * 
 * @author Albert Attard
 */
public class SearchForWordWorker extends SwingWorker<Integer, String> {

  private static void failIfInterrupted() throws InterruptedException {
    if (Thread.currentThread().isInterrupted()) {
      throw new InterruptedException("Interrupted while searching files");
    }
  }

  /** The word that is searched */
  private final String word;

  /**
   * The directory under which the search occurs. All text files found under the given directory are searched.
   */
  private final File directory;

  /** The text area where messages are written. */
  private final JTextArea messagesTextArea;

  /**
   * Creates an instance of the worker
   * 
   * @param word
   *          The word to search
   * @param directory
   *          the directory under which the search will occur. All text files found under the given directory
   *          are searched
   * @param messagesTextArea
   *          The text area where messages are written
   */
  public SearchForWordWorker(final String word, final File directory, final JTextArea messagesTextArea) {
    this.word = word;
    this.directory = directory;
    this.messagesTextArea = messagesTextArea;
  }

  @Override
  protected Integer doInBackground() throws Exception {
    // The number of instances the word is found
    int matches = 0;

    /*
     * List all text files under the given directory using the Apache IO library. This process cannot be
     * interrupted (stopped through cancellation). That is why we are checking right after the process whether
     * it was interrupted or not.
     */
    publish("Listing all text files under the directory: " + directory);
    final List<File> textFiles = new ArrayList<>(FileUtils.listFiles(directory, new SuffixFileFilter(".txt"),
        TrueFileFilter.TRUE));
    SearchForWordWorker.failIfInterrupted();
    publish("Found " + textFiles.size() + " text files under the directory: " + directory);

    for (int i = 0, size = textFiles.size(); i < size; i++) {
      /*
       * In order to respond to the cancellations, we need to check whether this thread (the worker thread)
       * was interrupted or not. If the thread was interrupted, then we simply throw an InterruptedException
       * to indicate that the worker thread was cancelled.
       */
      SearchForWordWorker.failIfInterrupted();

      // Update the status and indicate which file is being searched. 
      final File file = textFiles.get(i);
      publish("Searching file: " + file);

      /*
       * Read the file content into a string, and count the matches using the Apache common IO and Lang
       * libraries respectively.
       */
      final String text = FileUtils.readFileToString(file);
      matches += StringUtils.countMatches(text, word);

      // Update the progress
      setProgress((i + 1) * 100 / size);
    }

    // Return the number of matches found
    return matches;
  }

  @Override
  protected void process(final List<String> chunks) {
    // Updates the messages text area
    for (final String string : chunks) {
      messagesTextArea.append(string);
      messagesTextArea.append("\n");
    }
  }
}

Note that the swing worker takes a swing component (JTextArea) as an input to its constructor. This swing component is only accessed from the process() method and never used from within the doInBackbround() method or other methods directly (by directly we mean from the same thread) invoked from it. The following image highlights the places from where the swing component is accessed.

Accessing Swing Components from the Swing Worker

Accessing Swing Components from the Swing Worker

This is very important as all swing components should be only accessed from the event dispatcher thread.

Now we saw how to create a swing worker. In the next sections we will see how to start the worker and how to stop or better cancel the worker.

Starting the Swing Worker

The swing worker can be started by invoking the execute() method as highlighted in the following example.

  private void search() {
    final String word = wordTextField.getText();
    final File directory = new File(directoryPathTextField.getText());
    messagesTextArea.setText("Searching for word '" + word + "' in text files under: " + directory.getAbsolutePath()
        + "\n");
    searchWorker = new SearchForWordWorker(word, directory, messagesTextArea);
    searchWorker.addPropertyChangeListener(new PropertyChangeListener() {
      @Override
      public void propertyChange(final PropertyChangeEvent event) {
        switch (event.getPropertyName()) {
        case "progress":
          searchProgressBar.setIndeterminate(false);
          searchProgressBar.setValue((Integer) event.getNewValue());
          break;
        case "state":
          switch ((StateValue) event.getNewValue()) {
          case DONE:
            searchProgressBar.setVisible(false);
            searchCancelAction.putValue(Action.NAME, "Search");
            try {
              final int count = searchWorker.get();
              JOptionPane.showMessageDialog(Application.this, "Found: " + count + " words", "Search Words",
                  JOptionPane.INFORMATION_MESSAGE);
            } catch (final CancellationException e) {
              JOptionPane.showMessageDialog(Application.this, "The search process was cancelled", "Search Words",
                  JOptionPane.WARNING_MESSAGE);
            } catch (final Exception e) {
              JOptionPane.showMessageDialog(Application.this, "The search process failed", "Search Words",
                  JOptionPane.ERROR_MESSAGE);
            }

            searchWorker = null;
            break;
          case STARTED:
          case PENDING:
            searchCancelAction.putValue(Action.NAME, "Cancel");
            searchProgressBar.setVisible(true);
            searchProgressBar.setIndeterminate(true);
            break;
          }
          break;
        }
      }
    });
    searchWorker.execute();
  }

This will create a new thread from where the doInBackground() method is invoked.

Cancel the Worker

The swing worker can be stopped or better cancelled through the cancel() method. The swing worker provides a method called cancel which accepts a parameter of type boolean. This parameter determines whether or not the worker should be interrupted or not.

 
  private void cancel() {
    searchWorker.cancel(true);
  }

This will cause the worker’s get method to throw the exception CancellationException to indicate that the worker was forced cancellation.

Conclusion

Many GUI applications can be easily improved by taking advantage of the swing worker as we saw here. Any tasks that may take long time to execute, should always be performed through a swing worker as these will hinder the application responsiveness. Note that while testing, a developer may use a small search space which takes no time to execute, while in production the application will make use of a larger search space which will take longer to execute. Thus may make the application unresponsive until the long task is finished. The swing worker is not difficult to use and incorporate with existing code as shown in this article.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值