从Swing应用程序的EDT事件处理程序代码内部启动线程

时间:2019-09-25 12:58:58

标签: java multithreading swing java-8 event-dispatch-thread

我对Swing 事件调度程序线程(EDT)的理解是,它是执行事件处理代码的专用线程。因此,如果我的理解是正确的,那么在下面的示例中:

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
   }        
}

START EDTEND EDT之间的所有代码都在EDT上执行,而其外部的任何代码都在主应用程序线程上执行。同样,另一个示例:

// 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
});  

同样,在EDT中仅执行System.exit(0)

因此,对于初学者来说,如果我对EDT与主应用程序线程代码执行之间的“分工”理解不正确,请先纠正我!

现在,我遇到了一篇文章,该文章强调了从所有这些EDT代码内部创建新的Thread的用法,这将使我上面的第一个示例看起来像这样:

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
   }        
}

我的问题:这种方法有什么优势(或没有优势)?我应该以这种方式总是编码我的EDT代码,还是有一个需要遵循的规则作为何时应用的准则?预先感谢!

1 个答案:

答案 0 :(得分:5)

这个问题有点笼统和不确定,但是我将尝试解决您所提出的一些问题。进一步进行自己的研究的切入点可能是Lesson: Concurrency in Swing,尽管实际上可能很难从中得出针对特定情况的明确陈述。

首先,Swing中有一个总体规则-称为单线程规则

  

一旦实现了Swing组件,所有可能影响或依赖于该组件状态的代码都应在事件调度线程中执行。

(不幸的是,在本教程中不再如此明确地指出)


记住这一点,看看您的代码片段:

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

不幸的是,这是正确的,而且不幸的是,即使在某些Swing官方示例中,也是如此。但这可能已经引起问题。为了安全起见,应始终使用SwingUtilities#invokeLater在EDT上处理GUI(包括主机架)。模式始终是相同的:

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

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

关于您显示的涉及LabelUpdater类的第二个示例:我很好奇您从哪一篇文章中得到的。我知道,那里有很多cr4p,但是这个例子甚至没有意义...

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

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

如果此代码(即run方法)在新线程中执行,则显然违反了单线程规则JLabel是从不是事件分发线程的 线程修改的!


在事件处理程序中(例如,在actionPerformed的{​​{1}}方法中)启动新线程的主要目的是防止阻塞用户界面。如果您有这样的代码

ActionListener

然后按下按钮将导致EDT阻塞5分钟-即GUI将“冻结”,并且看起来像挂了。在这种情况下(例如,您需要长时间运行计算),您应该在自己的线程中完成工作。

手动执行此操作的幼稚方法 (大致)如下:

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

现在,按下按钮将启动一个新线程,并且GUI将不再阻塞。但是请注意代码中的someButton.addActionListener(e -> { startBackgroundThread(); }); private void startBackgroundThread() { Thread thread = new Thread(() -> { doSomeComputationThatTakesFiveMinutes(); someLabel.setText("Finished"); // WARNING - see notes below! }); thread.start(); } :现在又出现了一个问题,WARNING被一个不是事件分发线程的线程修改了!因此,您必须将其传递回EDT:

JLabel

这看起来笨拙而复杂,好像您可能很难确定当前使用的线程。没错。但是对于执行长时间运行任务的常见任务,有SwingWorker class explained in the tutorial使这种模式更简单。


无耻的自我促进:不久前,我创建了一个SwingTasks library,它基本上是“使用类固醇的摇摆工人”。它允许您“连接”这样的方法...

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

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

,如果执行时间过长,则会显示(模式)对话框,并提供其他一些便捷方法,例如用于在对话框中显示进度条等。样本汇总在https://github.com/javagl/SwingTasks/tree/master/src/test/java/de/javagl/swing/tasks/samples