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

hot*_*oup 1 java swing multithreading event-dispatch-thread java-8

我对Swing Event Dispatcher Thread(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
   }        
}
Run Code Online (Sandbox Code Playgroud)

在之间的所有代码START EDT,并END 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
});  
Run Code Online (Sandbox Code Playgroud)

同样,仅System.exit(0)在EDT内部执行。

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

然后,我遇到了一篇文章,该文章强调了Thread从所有这些EDT代码内部创建新的用法,这将使我上面的第一个示例如下所示:

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
   }        
}
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*o13 5

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

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

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

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


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

// OUTSIDE EDT
JFrame mainFrame = new JFrame("Java SWING Examples");
...
Run Code Online (Sandbox Code Playgroud)

不幸的是,甚至在某些Swing官方示例中,这通常都是正确的。但这可能已经引起问题。为了安全起见,应始终使用EDT在EDT上处理GUI(包括主机架)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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

    @Override
    public void run() {
        ...
        statusLabel.setText("Ok Button clicked.");
    }
}
Run Code Online (Sandbox Code Playgroud)

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


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

someButton.addActionListener(e -> {
    doSomeComputationThatTakesFiveMinutes();
    someLabel.setText("Finished");
});
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

现在,按下按钮将启动一个新线程,并且GUI将不再阻塞。但是请注意WARNING代码中的:现在又有一个问题,该问题JLabel是由不是事件分发线程的线程修改的!因此,您必须将其传递回EDT:

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

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

这看起来笨拙而复杂,似乎很难弄清楚当前使用的线程。没错。但是对于启动长期运行任务的常见任务,SwingWorker本教程介绍使此模式更简单。


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

SwingTaskExecutors.create(
    () -> computeTheResult(),
    result -> receiveTheResult(result)
).build().execute();
Run Code Online (Sandbox Code Playgroud)

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

  • 好吧,我们使用Swing,但我看不到任何真正的选择。对于开发缓慢(或几乎没有),我并不感到难过,因为这意味着在其之上构建的所有变通方法和工具将继续起作用,这比每隔几周破坏现有代码的新功能要好得多…… (2认同)