1. EDT (Event Dispatch Thread) là gì
Trong các ứng dụng đồ họa trên Java — dù là Swing hay JavaFX — mọi thao tác của người dùng (nhấp chuột, phím bấm), cũng như việc vẽ lại cửa sổ đều được xử lý trong một luồng đặc biệt gọi là EDT (Event Dispatch Thread, luồng phân phối sự kiện).
Tại sao cần nó? Các thành phần UI trong Java không an toàn theo luồng. Để tránh race condition và lỗi hiển thị, mọi thay đổi giao diện phải được thực hiện nghiêm ngặt ở một nơi — trong EDT. Nó giống như quầy thu ngân chỉ có một thu ngân: không thể có nhiều người xử lý cùng một hóa đơn cùng lúc.
Trong Swing, EDT chạy các bộ xử lý sự kiện và việc vẽ lại thành phần (ví dụ, actionPerformed). Trong JavaFX, tương đương là JavaFX Application Thread, nơi các cập nhật UI và các handler như setOnAction được thực thi.
2. Vấn đề “tác vụ dài” trong UI
Điều gì xảy ra nếu chạy một tác vụ dài trong EDT?
Khi người dùng nhấn nút, handler (ví dụ, actionPerformed hoặc setOnAction) chạy trên EDT. Nếu bên trong bạn khởi chạy một tác vụ nặng (đọc tệp lớn, yêu cầu mạng, tính toán phức tạp), toàn bộ UI sẽ “đóng băng”:
- Cửa sổ ngừng phản hồi với nhấp chuột và phím bấm.
- Việc vẽ lại ngừng hoạt động — khi di chuyển, cửa sổ “đơ”.
- Người dùng nghĩ rằng chương trình đã “hỏng”.
Ví dụ sai (Swing):
button.addActionListener(e -> {
// Tác vụ dài chạy ngay trong EDT!
longOperation(); // Ví dụ: đọc một tệp lớn
label.setText("Xong!");
});
Kết quả: trong khi longOperation() đang chạy, cửa sổ không phản hồi người dùng.
Vì sao? EDT xử lý các tác vụ theo hàng đợi và chỉ có thể thực thi một tác vụ tại một thời điểm. Khi nó bận với tác vụ dài của bạn, nó không thể xử lý nhấp chuột hay vẽ lại.
3. Giải pháp: tác vụ dài — chỉ chạy trong các luồng nền
Nguyên tắc:
- Mọi tác vụ dài — phải chạy trong các luồng nền.
- Mọi thay đổi UI — chỉ thực hiện trên EDT/JavaFX Application Thread.
Khởi chạy tác vụ dài trong một luồng riêng
Ví dụ (Swing):
button.addActionListener(e -> {
new Thread(() -> {
longOperation(); // Chạy trong luồng nền
// Bây giờ cần cập nhật UI — nhưng chỉ từ EDT!
SwingUtilities.invokeLater(() -> label.setText("Xong!"));
}).start();
});
Ví dụ (JavaFX):
button.setOnAction(e -> {
new Thread(() -> {
longOperation();
// Cập nhật UI qua Platform.runLater
Platform.runLater(() -> label.setText("Xong!"));
}).start();
});
Cập nhật UI từ luồng nền như thế nào?
- Swing: hãy dùng SwingUtilities.invokeLater(Runnable) — tác vụ sẽ được đưa vào hàng đợi EDT.
- JavaFX: hãy dùng Platform.runLater(Runnable) — tác vụ sẽ chạy trên JavaFX Application Thread.
Tại sao không thể đơn giản gọi label.setText(...) từ luồng nền? Vì đó là vi phạm tính an toàn luồng của UI: các thành phần chỉ được thay đổi từ luồng giao diện.
Các lớp chuyên dụng cho tác vụ nền
Trong các ứng dụng thực tế, ta thường cần hiển thị tiến độ, cho phép hủy và xử lý lỗi. Để làm điều đó có:
- SwingWorker<T, V> — cho Swing;
- Task<V>, Service<V> — cho JavaFX.
Ví dụ (JavaFX Task):
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
longOperation();
// Có thể cập nhật tiến độ: updateProgress(...)
return null;
}
};
task.setOnSucceeded(e -> label.setText("Xong!"));
task.setOnFailed(e -> label.setText("Lỗi!"));
new Thread(task).start();
Ưu điểm: có tiến độ, hủy, sự kiện thành công/lỗi. Thay đổi UI — thông qua các phương thức an toàn (updateMessage, updateProgress) hoặc các handler (setOnSucceeded và v.v.).
4. Mẫu đúng và sai
Sai: chạy tác vụ dài trong handler sự kiện
button.setOnAction(e -> longOperation()); // UI sẽ bị treo!
Đúng: chạy tác vụ dài trong một luồng riêng
button.setOnAction(e -> new Thread(() -> longOperation()).start());
Tốt hơn nữa: dùng 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("Xong!"));
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("Xong!");
}
};
worker.execute();
});
5. Thực hành: ví dụ tải tệp
JavaFX:
button.setOnAction(e -> {
Task<String> task = new Task<>() {
@Override
protected String call() throws Exception {
// Mô phỏng quá trình tải lâu
Thread.sleep(2000);
return "Tệp đã tải xong!";
}
};
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 "Tệp đã tải xong!";
}
@Override
protected void done() {
try {
label.setText(get());
} catch (Exception ex) {
label.setText("Lỗi!");
}
}
};
worker.execute();
});
6. Lỗi thường gặp khi làm việc với EDT và tác vụ dài
Lỗi số 1: Chạy tác vụ dài trong EDT. Toàn bộ ứng dụng “đóng băng”, cửa sổ không phản hồi, người dùng cho rằng chương trình bị hỏng.
Lỗi số 2: Cố cập nhật UI từ luồng nền. Vi phạm an toàn luồng của UI có thể dẫn tới bug, lỗi hiển thị và crash. Hãy dùng SwingUtilities.invokeLater hoặc Platform.runLater.
Lỗi số 3: Không xử lý lỗi trong tác vụ nền. Exception “bị mất”, người dùng không biết chuyện gì sai. Trong Swing — override done() và đọc get(); trong JavaFX — đăng ký setOnFailed.
Lỗi số 4: Không thể hủy tác vụ dài. Người dùng không thể dừng tải/tính toán. Hãy dùng cơ chế hủy (SwingWorker.cancel, Task.cancel) và kiểm tra cờ hủy bên trong tác vụ.
Lỗi số 5: Không hiển thị tiến độ. Người dùng nghĩ chương trình đang “treo”. Trong Swing — hãy dùng cơ chế publish kết quả và progress bar cùng với SwingWorker; trong JavaFX — updateProgress và các chỉ báo trực quan.
GO TO FULL VERSION