Starting threads from inside EDT event handler code in Swing apps

喜夏-厌秋 提交于 2021-02-19 08:11:26

问题


My understanding of the Swing Event Dispatcher Thread (EDT) is that its a dedicated thread where event handling code is executed. So, if my understanding is correct, then in the example below:

private class ButtonClickListener implements ActionListener{
   public void actionPerformed(ActionEvent e) {
      // START EDT
      String command = e.getActionCommand();  

      if( command.equals( "OK" ))  {
         statusLabel.setText("Ok Button clicked.");
      } else if( command.equals( "Submit" ) )  {
         statusLabel.setText("Submit Button clicked.");
      } else {
         statusLabel.setText("Cancel Button clicked.");
      }     
      // END EDT
   }        
}

All the code in between START EDT and END EDT is executing on the EDT, and any code outside of it is executing on the main application thread. Similarly, another example:

// OUTSIDE EDT
JFrame mainFrame = new JFrame("Java SWING Examples");
mainFrame.setSize(400,400);
mainFrame.setLayout(new GridLayout(3, 1));
mainFrame.addWindowListener(new WindowAdapter() {
   public void windowClosing(WindowEvent windowEvent){
      // START EDT
      System.exit(0);
      // END EDT
   }        
   // BACK TO BEING OUTSIDE THE EDT
});  

Again, only the System.exit(0) is executed inside the EDT.

So for starters, if my understanding of the "division of labor" between EDT and main app thread code execution is incorrect, please begin by correcting me!

Now then, I came across an article that emphasized the use of creating a new Thread from inside all this EDT code, which would make my first example above look like this:

public class LabelUpdater implements Runnable {
  private JLabel statusLabel;
  private ActionEvent actionEvent;

  // ctor omitted here for brevity

  @Override
  public void run() {
    String command = actionEvent.getActionCommand();  

    if (command.equals( "OK" ))  {
       statusLabel.setText("Ok Button clicked.");
    } else if( command.equals( "Submit" ) )  {
       statusLabel.setText("Submit Button clicked.");
    } else {
       statusLabel.setText("Cancel Button clicked.");
    }   
  }
}

private class ButtonClickListener implements ActionListener{
   public void actionPerformed(ActionEvent e) {
      // START EDT
      Thread thread = new Thread(new LabelUpdater(statusLabel, e));
      thread.start();
      // END EDT
   }        
}

My question: what advantage (or lack thereof) is there to this approach? Should I always code my EDT code this way, or is there a rubric one needs to follow as a guidelines for when to apply it? Thanks in advance!


回答1:


The question is a bit broad and unspecific, but I'll try to address some of the points that you asked about. The entry point for further, own research is probably the Lesson: Concurrency in Swing, although it may indeed be hard to derive definite statements for specific cases from that.

First of all, there is an overarching rule in Swing - referred to as the Single Thread Rule:

Once a Swing component has been realized, all code that might affect or depend on the state of that component should be executed in the event-dispatching thread.

(Unfortunately, it is no longer stated so clearly in the tutorial)


Keeping that in mind, looking at your snippets:

// OUTSIDE EDT
JFrame mainFrame = new JFrame("Java SWING Examples");
...

This is often true, unfortunately - and unfortunately, even in some of the official Swing examples. But this may already cause problems. To be on the safe side, the GUI (including the main frame) should always be handled on the EDT, using SwingUtilities#invokeLater. The pattern is always the same then:

public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> createAndShowGui());
}

private static void createAndShowGui() {
    JFrame mainFrame = new JFrame("Java SWING Examples");
    ...
    mainFrame.setVisible(true);
}

Regarding the second example that you showed, involving the LabelUpdater class: I'd be curious from which article you got this. I know, there is a lot of cr4p out there, but this example doesn't even remotely make sense...

public class LabelUpdater implements Runnable {
    private JLabel statusLabel;
    ...

    @Override
    public void run() {
        ...
        statusLabel.setText("Ok Button clicked.");
    }
}

If this code (i.e. the run method) is executed in an new thread, then it obviously violates the single thread rule: The status of the the JLabel is modified from a thread that is not the event dispatch thread!


The main point of starting a new thread in an event handler (e.g. in an actionPerformed method of an ActionListener) is to prevent blocking the user interface. If you had some code like this

someButton.addActionListener(e -> {
    doSomeComputationThatTakesFiveMinutes();
    someLabel.setText("Finished");
});

then pressing the button would cause the EDT to be blocked for 5 minutes - i.e. the GUI would "freeze", and look like it hung up. In these cases (i.e. when you have long-running computations), you should do the work in an own thread.

The naive approach of doing this manually could (roughly) look like this:

someButton.addActionListener(e -> {
    startBackgroundThread();
});

private void startBackgroundThread() {
    Thread thread = new Thread(() -> {
        doSomeComputationThatTakesFiveMinutes();
        someLabel.setText("Finished");              // WARNING - see notes below!
    });
    thread.start();
}

Now, pressing the button would start a new thread, and the GUI would no longer block. But note the WARNING in the code: Now there's this problem again of the JLabel being modified by a thread that is not the event dispatch thread! So you'd have to pass this back to the EDT:

private void startBackgroundThread() {
    Thread thread = new Thread(() -> {
        doSomeComputationThatTakesFiveMinutes();

        // Do this on the EDT again...
        SwingUtilities.invokeLater(() -> {
            someLabel.setText("Finished");
        });
    });
    thread.start();
}

This may look clumsy and complicated, and as if you could have a hard time figuring out on which thread you currently are. And that's right. But for the common task of starting a long-running task, there is the SwingWorker class explained in the tutorial that makes this pattern somewhat simpler.


Shameless self-promotion: A while ago, I created a SwingTasks library, which is basically a "Swing Worker on steroids". It allows you to "wire up" methods like this...

SwingTaskExecutors.create(
    () -> computeTheResult(),
    result -> receiveTheResult(result)
).build().execute();

and takes care of showing a (modal) dialog if the execution takes too long, and offers some other convenience methods, e.g. for showing a progress bar in the dialog and so on. The samples are summarized at https://github.com/javagl/SwingTasks/tree/master/src/test/java/de/javagl/swing/tasks/samples



来源:https://stackoverflow.com/questions/58099010/starting-threads-from-inside-edt-event-handler-code-in-swing-apps

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!