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

Starting threads from inside EDT event handler code in Swing apps

我对 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
});  

同样,只有 System.exit(0) 在 EDT 中执行。

所以对于初学者来说,如果我对EDT和主应用程序线程代码执行之间的"division of labor"理解不正确,请首先纠正我!

然后,我看到一篇文章,强调从所有这些 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 代码,还是需要遵循一个规则作为 何时 应用它的指南?提前致谢!

这个问题有点宽泛和不具体,但我会尝试解决您提出的一些问题。进一步研究的切入点可能是 Lesson: Concurrency in Swing,尽管可能确实很难从中得出针对特定案例的明确陈述。

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

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.

(遗憾的是,教程中不再说得那么清楚了)


记住这一点,看看你的片段:

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

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

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

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

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

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

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

如果这段代码(即run方法)是在一个新线程中执行的,那么显然违反了单线程规则: JLabel 的状态是从 不是 事件派发线程的线程修改的!


在事件处理程序中启动新线程(例如,在 ActionListeneractionPerformed 方法中)的主要目的是防止阻塞用户界面.如果你有这样的代码

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

然后按下按钮会导致 EDT 被阻塞 5 分钟 - 即 GUI 会 "freeze",看起来像是挂断了。在这些情况下(即,当您进行长时间 运行 计算时),您应该在自己的线程中完成工作。

手动执行此操作的天真方法可能(大致)如下所示:

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

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

现在,按下按钮将启动一个新线程,并且 GUI 将不再阻塞。但是请注意代码中的 WARNING:现在又出现了 JLabel 而不是 事件派发线程的线程修改的问题!所以你必须把这个传回给美国东部时间:

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

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

这可能看起来笨拙和复杂,好像您很难弄清楚您当前在哪个线程上。没错。但是对于启动一个长 运行 任务的常见任务,SwingWorker class explained in the tutorial 使这个模式稍微简单一些。


无耻的自我推销:前段时间,我创建了一个SwingTasks library,基本上是一个"Swing Worker on steroids"。它允许您 "wire up" 这样的方法...

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

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