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