从 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 EDT
和 END 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
的状态是从 不是 事件派发线程的线程修改的!
在事件处理程序中启动新线程(例如,在 ActionListener
的 actionPerformed
方法中)的主要目的是防止阻塞用户界面.如果你有这样的代码
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
我对 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 EDT
和 END 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
的状态是从 不是 事件派发线程的线程修改的!
在事件处理程序中启动新线程(例如,在 ActionListener
的 actionPerformed
方法中)的主要目的是防止阻塞用户界面.如果你有这样的代码
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