Rethinking Swing Threading

来源:互联网 发布:网络诈骗小品剧本7人 编辑:程序博客网 时间:2024/05/03 00:05
<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 728x15, 创建于 08-4-23MSDN */google_ad_slot = "3624277373";google_ad_width = 728;google_ad_height = 15;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 160x600, 创建于 08-4-23MSDN */google_ad_slot = "4367022601";google_ad_width = 160;google_ad_height = 600;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>

  Improper Swing Threading is one of the main causes of sluggish, unresponsive, and unstable Swing applications. There are many reasons for this, from developers not understanding the Swing single Threading model, to the difficulty of ensuring proper thread execution. Even when a conscious effort is given to Swing Threading, application-Threading logic tends to get quite difficult to understand and maintain. This article explains how to use event-driven programming to develop Swing applications, resulting in greatly simplified development, maintenance, and flexibility.

  Background

  Since we are trying to simplify the Threading of Swing applications, let's first take a quick look at how Swing Threading works and why it's necessary. The Swing API is designed around the single Threading model. This means that Swing components must always be modified and otherwise interacted with using the same thread. There are a number of reasons for the single thread model, including the development cost and complexity of synchronizing Swing -- an already slow API. To facilitate the single Threading model, there is a dedicated thread for interacting with Swing components. This thread is known as the Swing thread, the AWT (sometimes pronounced "ought") thread, or the event-dispatch thread. For the rest of this article, I'll refer to is as the Swing thread.

  Since the Swing thread is the only thread that should interact with Swing components, it has a lot of responsibilities. All painting and graphics, mouse events, component events, button events, and all other events occur in the Swing thread. Since the Swing thread is already weighted down with work, problems occur when too much other work is executed in the Swing thread. One of the most common places this can occur is in the placement of non-Swing work, like a database lookup, in an event listener method, such as an ActionListener on a JButton. Since the ActionListener's actionPerformed() method automatically gets performed in the Swing thread, the database call is also executed in the Swing thread. This occupies the Swing thread with work, preventing it from performing its other responsibilities -- like painting, responding to mouse movements, processing button events, and application resizing. Users think the application is frozen, but it may not be. Executing the code in the appropriate thread is essential to guarantee that the system is executing properly.

  Now that we've taken a look at why it is important to execute Swing application code in the appropriate thread, let's take a look at how Threading is often implemented. We'll look at the standard mechanisms for moving code into and out of the Swing thread. In the process, I'll highlight some of the problems and difficulties with the standard approach. As we'll see, most of the problems come from attempting to implement a synchronous code model with the asynchronous Swing Threading model. From there, we will see how to modify our example to be event-driven -- migrating the entire approach to an asynchronous model.

  Common Swing Threading Solution

  Let's start by looking at one of the most common Swing Threading mistakes. We will try to fix this problem using the standard techniques. In the process, you will see the complexity and common difficulties of implementing correct Swing Threading. Also, note that in the process of fixing this Threading problem, many of the intermediate examples will also not work. In the example, I note where the code breaks with a code comment starting with //broken. So now, let's get to our example.

  Assume that we are performing book searches. We have a simple user interface with a search text field, a search button, and an output text area. This interface is shown in Figure 1. Don't hold me to the UI design. This is pretty ugly, I agree.

 

  Figure 1. Basic query UI

  The user enters a book title, author, or other criteria, and a list of results is displayed. The following code sample shows the button's ActionListener calling the lookup() method in the same thread. For these examples, I am using a stubbed out-lookup where I call thread.sleep() for five seconds. The result of the sleeping thread is the same as a synchronous server call that lasts five seconds.

  private void searchButton_actionPerformed() {

  outputTA.setText("Searching for: " +

  searchTF.getText());

  //Broken!! Too much work in the Swing thread

  String[] results = lookup(searchTF.getText());

  outputTA.setText("");

  for (int i = 0; i < results.length; i++) {

  String result = results[i];

  outputTA.setText(outputTA.getText() +

  '/n' + result);

  }

  }

  If you run this code (the complete source is available for download), there are a few things that you will immediately notice are wrong. Figure 2 shows a screen shot of the search running.

 

  Figure 2. Doing the search in the Swing thread

  Notice that the Go button appears "pressed." This is because the actionPerformed method, which notifies the button to be repainted in its non-pressed look, has not returned. Also, notice that the search string "abcde" is not displayed in the text area. The first line of the searchButton_actionPerformed method sets the text area text to the search string. But remember that Swing repaints are not immediate. Rather, a repaint request is placed on the Swing event queue for the Swing thread to process. But here, we are occupying the Swing thread with our lookup, so it can't process the repaint.

  To fix these and other problems, let's move to the lookup to a non-Swing thread. The first tendency is to have the entire method execute in a new thread. The problem with this is that the Swing components, in this case the output text area, can only be edited from the Swing thread. Here is the modified searchButton_actionPerformed method:

  private void searchButton_actionPerformed() {

  outputTA.setText("Searching for: " +

  searchTF.getText());

  //the String[][] is used to allow access to

  // setting the results from an inner class

  final String[][] results = new String[1][1];

  new Thread(){

  public void run() {

  results[0] = lookup(searchTF.getText());

  }

  }.start();

  outputTA.setText("");

  for (int i = 0; i < results[0].length; i++) {

  String result = results[0][i];

  outputTA.setText(outputTA.getText() +

  '/n' + result);

  }

  }

  There are a few problems with this. Notice the final String[][]. This is an unfortunate artifact involved with anonymous inner classes and scope. Basically, any variable used in an anonymous inner class but defined in the containing class scope needs to be declared final. You can get around this by making an array to hold the variable. This way, you can make the array final and modify the element in the array but not the array reference itself. Now that we're done with the minutiae, let's get to the real problem. Figure 3 shows what happens when this code is run:

 

  Figure 3. Doing the search outside of the Swing thread

  The display shows a null because the display code is processed before the lookup code completes! This is because the code block continues execution once the new thread is started, not when it's done executing. This is one of those strange-looking concurrent code blocks where code later on in a method can actually execute before code earlier in a method.

  There are two methods in the SwingUtilities class that can help us out here: invokeLater() and invokeAndWait(). Each method takes a Runnable and executes it in the Swing thread. The invokeAndWait() method blocks until the Runnable completes execution, and invokeLater() executes the Runnable asynchronously. Using invokeAndWait() is generally frowned upon, since it can cause severe thread deadlocks that can wreak havoc on your application. So let's just put it aside and use the invokeLater() method.

  To fix the last problem with variable scooping and order of execution, we have to move the text-area getText() and setText() calls into a Runnable that executes only when the results are returned, and executes in the Swing thread. We can do this by creating an anonymous Runnable that we pass to invokeLater(), containing the text area manipulation from the end of the new Thread's Runnable. This guarantees that the Swing code will not execute before the lookup completes. Here is the code:

  private void searchButton_actionPerformed() {

  outputTA.setText("Searching for: " +

  searchTF.getText());

  final String[][] results = new String[1][1];

  new Thread() {

  public void run() {

  //get results.

  results[0] = lookup(searchTF.getText());

  // send runnable to the Swing thread

  // the runnable is queued after the

  // results are returned

  SwingUtilities.invokeLater(

  new Runnable() {

  public void run() {

  // Now we're in the Swing thread

  outputTA.setText("");

  for (int i = 0;

  i < results[0].length;

  i++) {

  String result = results[0][i];

  outputTA.setText(

  outputTA.getText() +

  '/n' + result);

  }

  }

  }

  );

  }

  }.start();

  }

  This will work. But it was a major headache to get here. We had to pay serious attention to the order of execution through anonymous Threads, and we had to deal with difficult scooping issues. These are not rare problems. Additionally, this is a pretty simple example, and we've already had major issues with scope, variable passing, and order of execution. Imagine more complex problems where there are several levels of nesting, with shared references and a designated order of execution. This approach quickly gets out of hand.

  The Problem

  We are trying to force synchronous execution through an asynchronous model -- trying to fit a square peg in a round hole. As long as we're trying to do this, we will continue to encounter these problems. From experience, I can tell you this code will be hard to write, hard to maintain, and very error-prone.

  This seems like a common problem, so there must be standard ways to solve this, right? There are several frameworks including one that I wrote but was never publicly released. I called it the Chained

  Runnable Engine ad it suffered from similar synchronous-versus-asynchronous problems. Using this framework, you would create a collection of Runnables that would be executed by the engine. Each Runnable had an indicator telling the engine whether to execute it in the Swing thread or an alternate thread. The engine also ensured that each Runnable executed in proper order. So Runnable #2 would not be queued until Runnable #1 completed. And finally, it supported variable passing in the form of a HashMap that was passed from Runnable to Runnable.

  On this surface, this looks like it solves our main problems. But once you start to dig deeper, the same problems arise. Essentially, we haven't changed anything from what has been described above -- we would just be hiding some of the complexity in the engine. The code was very tedious to write and was quite complex, due to a seemingly exponential number of Runnables, which often ended up being tightly coupled. The non-typed HashMap variable passing between Runnables became hard to manage. The list goes on.

  After working on this framework, I realized this requires a completely different solution. This led me to reexamine the problem, look at how others are solving similar problems, and take a close look at the Swing source.

 

 

 

 

  The Solution: Event-Driven Programming

  All of the previous solutions share the same fatal flaw -- trying to represent a functional set of tasks while continuously changing threads. But changing threads requires an asynchronous model, since threads process Runnables asynchronously. Part of the problem is that we are trying to implement a synchronous model of a series of functions on top of an asynchronous Threading model. That is the reason for all of the chaining and dependencies between Runnables; the order of execution and inner-class scooping issues. If we can make this truly asynchronous, we can solve our problem and simplify Swing Threading tremendously.

  Before we go on, let's just enumerate the problems we are trying to solve:

  Execute code in the appropriate thread.

  Asynchronous execution using SwingUtilities.invokeLater().

  And asynchronous execution causes the following problems:

  Coupled components.

  Difficult variable passing.

  Order of execution.

  Let's think for a minute about message-based systems like Java Messaging Service (JMS), since they promote loosely coupled components functioning in an asynchronous environment. Messaging systems fire asynchronous events into the system, as described at the Enterprise Integration Patterns site. Interested parties listen for that event and react to it -- usually by performing some work of their own. The result is a set of modular, loosely coupled components that can be added to and removed from the system without affecting the rest of the system. But more importantly, dependencies between components are minimized, since each component is well defined and encapsulated -- each responsible for its own work. They simply fire messages to which the other components respond, and respond to messages that have been fired.

  For now, let's ignore the Threading issue and work on decoupling and moving to an asynchronous environment. After we've solved the asynchronous problems, we'll go back and take a look at the Threading issue. As we'll see, solving it at that point will be much easier.

  Let's take our example from the first section and begin migrating it to an event-based model. To get started, let's abstract the lookup call into a class called LookupManager. This will enable us to move all of the database logic out of the UI class and will eventually allow us to completely decouple the two. Here is the code for the LookupManager class:

  class LookupManager {

  private String[] lookup(String text) {

  String[] results = ...

  // database lookup code

  return results

  }

  }

  Now we'll start to move towards an asynchronous model. To make this call asynchronous, we need to abstract the call from the return. In other words, methods can't return anything. We'll start by deciding what the relevant actions are that other classes might want to know about. The obvious event in our case is the completion of the search. So let's create a listener interface reflecting these actions. The interface will have a single method called lookupCompleted(). Here is the interface:

  interface LookupListener {

  public void lookupCompleted(Iterator results);

  }

  Following the Java standard, we'll create another class called LookupEvent to contain the result String array rather than passing the String array around directly. This will also allow us flexibility down the road to pass other information without changing the LookupListener interface. For example, we could include the search string along with the results. Here is the LookupEvent class:

  public class LookupEvent {

  String searchText;

  String[] results;

  public LookupEvent(String searchText) {

  this.searchText = searchText;

  }

  public LookupEvent(String searchText,

  String[] results) {

  this.searchText = searchText;

  this.results = results;

  }

  public String getSearchText() {

  return searchText;

  }

  public String[] getResults() {

  return results;

  }

  }

  Notice that the LookupEvent class is immutable. This is important, since we are unaware who will be processing these events down the road. And unless we are willing to make a defensive copy of the event that we send to each listener, we need to make the event immutable. If not, a listener could unintentionally or maliciously modify the event and break the system.

  Now we need to call the lookupComplete() event from LookupManager. We'll start by adding a collection of LookupListeners to LookupManager:

  List listeners = new ArrayList();

  And we'll add methods to add and remove LookupListeners from LookupManager:

  public void addLookupListener(LookupListener listener){

  listeners.add(listener);

  }

  public void removeLookupListener(LookupListener listener){

  listeners.remove(listener);

  }

  We need to call the listeners from the code when the action occurs. In our example, we'll fire a lookupCompleted() event when the lookup returns. This means iterating through the list of listeners and calling their lookupCompleted() methods with a LookupEvent.

  I like to extract this code to a separate method called fire[event-method-name] that constructs an event, iterates through the listeners, and calls the appropriate methods on the listeners. It helps to isolate the code for calling the listeners from the main logic. Here is our fireLookupCompleted method:

  private void fireLookupCompleted(String searchText,

  String[] results){

  LookupEvent event =

  new LookupEvent(searchText, results);

  Iterator iter =

  new ArrayList(listeners).iterator();

  while (iter.hasNext()) {

  LookupListener listener =

  (LookupListener) iter.next();

  listener.lookupCompleted(event);

  }

  }

  The second line creates a new collection, passing it the collection of listeners from which to create the array. This is in case the listener decides to remove itself from the LookupManager as a result of the event. If we don't safely copy the collection, we'll get nasty errors where listeners are not called when they should be.

  Next, we'll call the fireLookupCompleted() helper method from the point that the action is completed. In this case, it's the end of the lookup method when the results are returned. So we can change the lookup method to fire an event rather than return the String array itself. Here is the new lookup method:

  public void lookup(String text) {

  //mimic the server call delay...

  try {

  Thread.sleep(5000);

  } catch (Exception e){

  e.printStackTrace();

  }

  //imagine we got this from a server

  String[] results =

  new String[]{"Book one",

  "Book two",

  "Book three"};

  fireLookupCompleted(text, results);

  }

  Now let's add our listener to LookupManager. We want to update the text area when the lookup returns. Previously, we just called the setText() method directly, since the text area was in local as the database calls were being done in the UI. Now that we've abstracted the lookup logic out from the UI, we'll make the UI class a listener to the LookupManager to listen for lookup events and update itself accordingly. First, we'll implement the listener in the class declaration:

  public class FixedFrame implements LookupListener

  Then we'll implement the interface method

  public void lookupCompleted(final LookupEvent e) {

  outputTA.setText("");

  String[] results = e.getResults();

  for (int i = 0; i < results.length; i++) {

  String result = results[i];

  outputTA.setText(outputTA.getText() +

  "/n" + result);

  }

  }

  Finally, we'll register it as a listener to the LookupManager.

  public FixedFrame() {

  lookupManager = new LookupManager();

  //here we register the listener

  lookupManager.addListener(this);

  initComponents();

  layoutComponents();

  }

  For simplicity, I added it as a listener in the class constructor. This works fine for most systems. As systems get more complicated, you may want to refactor and abstract the listener registration out of constructors, allowing for greater flexibility and extensibility.

  Now that you can see everything connected, notice the separation of responsibilities. The user interface class is responsible for the display of information -- and only the display of information. The LookupManager class, on the other hand, is responsible for all lookup connections and logic. Additionally, LookupManager is responsible for notifying listeners when it changes -- but not what it should do when those changes occur. This allows you to connect an arbitrary set of listeners.

  To see how to add new events, let's go back and add an event for starting a lookup. We can add an event to our LookupListener called lookupStarted() that we will fire before the lookup is executed. Let's also create a fireLookupStarted() event calling lookupStarted() in all of the LookupListeners. Now the lookupMethod looks like this:

  public void lookup(String text) {

  fireLookupStarted(text);

  //mimic the server call delay...

  try {

  Thread.sleep(5000);

  } catch (Exception e){

  e.printStackTrace();

  }

  //imagine we got this from a server

  String[] results =

  new String[]{"Book one",

  "Book two",

  "Book three"};

  fireLookupCompleted(text, results);

  }

  And we'll add the new fire method, fireLookupStarted(). This method is identical to the fireLookupCompleted() method except that we are calling the lookupStarted() method on the listener, and that the event does not have a result set yet. Here is the code:

  private void fireLookupStarted(String searchText){

  LookupEvent event =

  new LookupEvent(searchText);

  Iterator iter =

  new ArrayList(listeners).iterator();

  while (iter.hasNext()) {

  LookupListener listener =

  (LookupListener) iter.next();

  listener.lookupStarted(event);

  }

  }

  And finally, we'll implement the lookupStarted() method in the UI that will set the text area to reflect the current search string.

  public void lookupStarted(final LookupEvent e) {

  outputTA.setText("Searching for: " +

  e.getSearchText());

  }

  This example shows the ease of adding new events. Now, let's look at an example that shows the flexibility of the event-driven decoupling. We'll do this by creating a logger class that prints a statement out to the command line whenever a search is started or completed. We'll call the class Logger. Here is the code:

  public class Logger implements LookupListener {

  public void lookupStarted(LookupEvent e) {

  System.out.println("Lookup started: " +

  e.getSearchText());

  }

  public void lookupCompleted(LookupEvent e) {

  System.out.println("Lookup completed: " +

  e.getSearchText() +

  " " +

  e.getResults());

  }

  }

  Now, we'll add the Logger as a listener to the LookupManager in the FixedFrame constructor:

  public FixedFrame() {

  lookupManager = new LookupManager();

  lookupManager.addListener(this);

  lookupManager.addListener(new Logger());

  initComponents();

  layoutComponents();

  }

  Now you've seen examples of adding new events as well as creating new listeners -- showing you the flexibility and extensibility of the event-driven approach. You'll find that as you develop more with event-centered programs, you start to get a better feeling for creating generic actions that are used throughout your application. Like anything else, it just takes some time and experience. And it seems like a lot of work up front to set up the event model, but you have to weigh it against the consequences of other alternatives. Consider the development time cost; first of all, it's a one-time cost. Adding listeners later to your applications, once you set up your listener model and their actions, is trivial.

  Threading

  At this point, we've solved our stated asynchronous problems; decoupled components through listeners, variable passing through event objects, and order of execution through a combination of event generation and registered listeners. With that behind us, let's get back to the Threading issue, since that's what brought us here in the first place. It's actually quite easy: since we have asynchronously functioning listeners, we can simply have the listeners themselves decide what thread they execute in. Think about the separation between the UI class and the LookupManager. The UI class is deciding what kind of processing to do, based on the event. Also, that class is all Swing, whereas a logging class would not be. So it makes a lot of sense to have the UI class be responsible for which thread it executes in.

  So let's take a look at our UI class again. Here is the lookupCompleted() method without Threading:

  public void lookupCompleted(final LookupEvent e) {

  outputTA.setText("");

  String[] results = e.getResults();

  for (int i = 0; i < results.length; i++) {

  String result = results[i];

  outputTA.setText(outputTA.getText() +

  "/n" + result);

  }

  }

  We know that this is going to be called from a non-Swing thread, since the events are being fired directly from the LookupManager, which cannot be executing code in the Swing thread. Since all of the code is functioning asynchronously (we don't have to wait for the listener method to complete to invoke any other code), we can redirect the code into the Swing thread using SwingUtilities.invokeLater(). Here is the new method, passing an anonymous Runnable to SwingUtilities.invokeLater():

  public void lookupCompleted(final LookupEvent e) {

  //notice the Threading

  SwingUtilities.invokeLater(

  new Runnable() {

  public void run() {

  outputTA.setText("");

  String[] results = e.getResults();

  for (int i = 0;

  i < results.length;

  i++) {

  String result = results[i];

  outputTA.setText(outputTA.getText() +

  "/n" + result);

  }

  }

  }

  );

  }

  If any LookupListeners are not executing in the Swing thread, we can execute in the listener code in the calling thread. As a rule of thumb, we want all of the listeners to be notified quickly. So if you have a listener that is going to take a lot of time to complete its functionality, you may want to create a new Thread or send the time consuming code off to a ThreadPool for execution.

  The last step is to make the LookupManager perform the lookup in a non-Swing thread. Currently, the LookupManager is being called from a Swing thread in the JButton's ActionListener. Now we have a decision to make; either we can introduce a new thread in the JButton's ActionListener, or we could ensure that the lookup method itself guarantees that it is being executed in a non-Swing thread and starts a thread of its own. I prefer to manage Swing Threading as close to the Swing classes as possible. This helps encapsulate all Swing logic together. If we added Swing Threading logic to the LookupManager, we are introducing a level of dependency that is not necessary. Additionally, it is completely unnecessary for the LookupManager to spawn its own thread in a non-Swing context, such as a headless (non-graphical) user interface or, in our example, the Logger. Spawning new threads unnecessarily would only hurt your applications' performance, rather than help it. The lookup manager executes perfectly fine regardless of Swing Threading -- so I like to keep the code out of there.

  Now we need to make the JButton's ActionListener execute the lookup in a non-Swing thread. We'll create an anonymous Thread with an anonymous Runnable that executes the lookup.

  private void searchButton_actionPerformed() {

  new Thread(){

  public void run() {

  lookupManager.lookup(searchTF.getText());

  }

  }.start();

  }

  This completes our Swing Threading. Simply adding the thread in actionPerformed() method and making sure the listeners are executing in the new thread takes care of the whole Threading issue. Notice we didn't deal with any problems like the first examples. By spending our time defining an event-driven architecture, we save that time and more when it comes to Swing Threading.

  Conclusion

  If you need to execute a lot of Swing code and non-Swing code in the same method, there is likely to be some code in the wrong place. The event-driven approach forces you to place code where it belongs -- and only where it belongs. If you have a method that is executing a database call and updating UI components in the same method, you have too much logic in one class. The process of going through and analyzing the events of your system and creating an underlying event model forces you to put code only where it belongs. Code for expensive database calls does not belong in UI classes; nor do UI component updates belong in non-UI classes. With an event-driven architecture, the UI is responsible for UI updates and some database manager is responsible for database calls. At that point, each encapsulated class worries about its own Threading, with minimal concern about how the rest of the system is functioning. It certainly takes more effort up front to design and build an event-driven client, but over time, that up-front cost is far outweighed by the flexibility and maintainability of the resulting system.

  Jonathan Simon is a comprehensive client side expert who designs and develops mission critical financial trading clients for Wall Street investment

<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 728x15, 创建于 08-4-23MSDN */google_ad_slot = "3624277373";google_ad_width = 728;google_ad_height = 15;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
<script type="text/javascript"><!--google_ad_client = "pub-2947489232296736";/* 160x600, 创建于 08-4-23MSDN */google_ad_slot = "4367022601";google_ad_width = 160;google_ad_height = 600;//--></script><script type="text/javascript"src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
原创粉丝点击