1. 什么是 EDT (Event Dispatch Thread)
在 Java 图形应用中——无论是 Swing 还是 JavaFX——所有用户操作(点击、按键)以及窗口重绘都在一个名为 EDT 的专用线程中处理(Event Dispatch Thread,事件派发线程)。
为什么需要它? Java 的 UI 组件不是线程安全的。为避免竞态条件与界面伪影,所有对界面的修改都必须在同一个地方执行——即 EDT。这就像只有一名收银员的收银台:同一张小票不可能被多人同时处理。
在 Swing 中,EDT 负责执行事件处理器和组件重绘(例如 actionPerformed)。在 JavaFX 中,对应的是 JavaFX Application Thread,UI 更新和诸如 setOnAction 等处理器都在其上执行。
2. UI 中的“耗时操作”问题
如果在 EDT 中启动耗时操作会发生什么?
当用户点击按钮时,处理器(例如 actionPerformed 或 setOnAction)是在 EDT 上执行的。如果其中启动了重任务(读取大文件、网络请求、复杂计算),整个 UI 会“卡住”:
- 窗口不再响应点击和按键。
- 重绘停止——拖动时窗口会“粘住”。
- 用户会认为程序“挂了”。
错误示例(Swing):
button.addActionListener(e -> {
// 在 EDT 中直接执行耗时操作!
longOperation(); // 例如,读取大型文件
label.setText("完成!");
});
结果:在执行 longOperation() 期间,窗口对用户无任何响应。
为什么?EDT 按队列处理任务,一次只能执行一个。当它被你的耗时操作占用时,就无法处理点击或重绘。
3. 解决方案:耗时操作只放在后台线程
原则:
- 所有耗时操作都放到后台线程。
- 所有 UI 更改都在 EDT/JavaFX Application Thread 中进行。
在单独线程中启动耗时操作
示例(Swing):
button.addActionListener(e -> {
new Thread(() -> {
longOperation(); // 在后台线程中执行
// 现在需要更新 UI——但必须在 EDT 中!
SwingUtilities.invokeLater(() -> label.setText("完成!"));
}).start();
});
示例(JavaFX):
button.setOnAction(e -> {
new Thread(() -> {
longOperation();
// 通过 Platform.runLater 更新 UI
Platform.runLater(() -> label.setText("完成!"));
}).start();
});
如何在后台线程中更新 UI?
- Swing:使用 SwingUtilities.invokeLater(Runnable)——任务会进入 EDT 的队列。
- JavaFX:使用 Platform.runLater(Runnable)——任务会在 JavaFX Application Thread 中执行。
为什么不能直接 从后台线程调用 label.setText(...)?因为这会破坏 UI 的线程安全:组件只能在界面线程中修改。
用于后台任务的专用类
在真实应用中,常常需要显示进度、支持取消并处理错误。可使用以下工具:
- SwingWorker<T, V>——用于 Swing;
- Task<V>、Service<V>——用于 JavaFX。
示例(JavaFX Task):
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
longOperation();
// 可以更新进度:updateProgress(...)
return null;
}
};
task.setOnSucceeded(e -> label.setText("完成!"));
task.setOnFailed(e -> label.setText("出错!"));
new Thread(task).start();
优点:进度、取消、成功/失败事件。UI 的更改可通过安全方法(如 updateMessage、updateProgress)或处理器(setOnSucceeded 等)完成。
4. 正确与错误的模式
错误:在事件处理器中执行耗时操作
button.setOnAction(e -> longOperation()); // UI 会卡死!
正确:在单独线程中执行耗时操作
button.setOnAction(e -> new Thread(() -> longOperation()).start());
更好的做法:使用 Task/Worker
JavaFX:
button.setOnAction(e -> {
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
longOperation();
return null;
}
};
task.setOnSucceeded(ev -> label.setText("完成!"));
new Thread(task).start();
});
Swing:
button.addActionListener(e -> {
SwingWorker<Void, Void> worker = new SwingWorker<>() {
@Override
protected Void doInBackground() throws Exception {
longOperation();
return null;
}
@Override
protected void done() {
label.setText("完成!");
}
};
worker.execute();
});
5. 实战:文件加载示例
JavaFX:
button.setOnAction(e -> {
Task<String> task = new Task<>() {
@Override
protected String call() throws Exception {
// 模拟耗时加载
Thread.sleep(2000);
return "文件已加载!";
}
};
task.setOnSucceeded(ev -> label.setText(task.getValue()));
new Thread(task).start();
});
Swing:
button.addActionListener(e -> {
SwingWorker<String, Void> worker = new SwingWorker<>() {
@Override
protected String doInBackground() throws Exception {
Thread.sleep(2000);
return "文件已加载!";
}
@Override
protected void done() {
try {
label.setText(get());
} catch (Exception ex) {
label.setText("出错!");
}
}
};
worker.execute();
});
6. 使用 EDT 与耗时操作时的常见错误
错误 1:在 EDT 中执行耗时操作。 整个应用会“卡住”,窗口无响应,用户会认为程序挂了。
错误 2:尝试从后台线程更新 UI。 破坏 UI 的线程安全可能导致 Bug、伪影和崩溃。请使用 SwingUtilities.invokeLater 或 Platform.runLater。
错误 3:后台任务缺少错误处理。 异常会“丢失”,用户不知道哪里出了问题。在 Swing 中——重写 done() 并调用 get();在 JavaFX 中——监听 setOnFailed。
错误 4:无法取消耗时操作。 用户无法中断加载/计算。请使用取消支持(SwingWorker.cancel、Task.cancel),并在任务内部检查取消标志。
错误 5:没有进度指示。 用户会以为程序“卡住”。在 Swing 中——使用结果发布与进度条配合 SwingWorker;在 JavaFX 中——使用 updateProgress 和可视化指示器。
GO TO FULL VERSION