CodeGym /Các khóa học /JAVA 25 SELF /Luồng EDT và các thao tác dài trong UI

Luồng EDT và các thao tác dài trong UI

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

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.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Sự kiện và xử lý sự kiện
Sự kiện và xử lý sự kiện
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION