首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java Swing 多執行緒實現等待提示對話框:避免 UI 凍結的最佳實踐

     編輯:Java 程式設計入門

前言:Swing 事件分發執行緒 (EDT) 的單執行緒規則

Swing 不是執行緒安全的,**所有 UI 操作必須在事件分發執行緒 (Event Dispatch Thread, EDT) 執行**。當在 EDT 中執行耗時任務 (資料庫批次操作、複雜運算、遠程呼叫) 時,EDT 被阻塞,導致介面無法重繪、無法響應點擊、**視覺上呈現「凍結/假死」**。本文展示如何利用 SwingWorker (Java 6+) 正確實現「後台執行緒執行任務 + 前台顯示等待對話框 + 完成自動關閉」模式,並對比舊版手工執行緒管理的缺陷。

一、問題場景與錯誤示範


// ❌ 錯誤:在 EDT 中直接執行耗時操作
btnStart.addActionListener(e -> {
    // 這裡會阻塞 EDT,介面凍結,進度條不轉、無法移動視窗
    for (int i = 0; i < 100000; i++) System.out.println(i); 
    JOptionPane.showMessageDialog(frame, "完成");
});

二、現代標準解法:SwingWorker

javax.swing.SwingWorker 專為 Swing 異步任務設計:

  • doInBackground():在**工作者執行緒**執行耗時邏輯,返回結果類型 T。
  • process(List):在 **EDT** 處理 publish(V...) 發布的中間結果 (更新進度條、列表)。
  • done():任務完成 (正常/異常/取消) 後在 **EDT** 自動調用,安全更新 UI。
  • get():阻塞等待結果 (僅在非 EDT 呼叫)。
  • 內建 PropertyChangeListener 支援進度屬性 (progress 0-100)。

三、完整實戰範例:可取消的等待對話框


import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.List;

public class WaitDialogDemo extends JFrame {
    private JButton btnStart;
    private JProgressBar progressBar;
    private SwingWorker worker;

    public WaitDialogDemo() {
        setTitle("SwingWorker 等待對話框演示");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(400, 200);
        setLocationRelativeTo(null);
        setLayout(new BorderLayout(10, 10));

        btnStart = new JButton("開始耗時任務 (模擬 5 秒)");
        progressBar = new JProgressBar(0, 100);
        progressBar.setStringPainted(true);
        progressBar.setVisible(false);

        JPanel panel = new JPanel(new BorderLayout(5, 5));
        panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
        panel.add(btnStart, BorderLayout.NORTH);
        panel.add(progressBar, BorderLayout.CENTER);
        add(panel);

        btnStart.addActionListener(this::startTask);
    }

    private void startTask(ActionEvent e) {
        btnStart.setEnabled(false);
        progressBar.setValue(0);
        progressBar.setVisible(true);

        // 1. 建立 SwingWorker
        worker = new SwingWorker() {
            @Override
            protected Void doInBackground() throws Exception {
                // 模擬耗時任務:分 100 步,每步 50ms
                for (int i = 1; i <= 100; i++) {
                    if (isCancelled()) break; // 支援取消
                    Thread.sleep(50); // 耗時操作
                    publish(i); // 發布進度 (觸發 process)
                }
                return null;
            }

            @Override
            protected void process(List chunks) {
                // 在 EDT 執行,安全更新 UI
                int latest = chunks.get(chunks.size() - 1);
                progressBar.setValue(latest);
            }

            @Override
            protected void done() {
                // 在 EDT 執行,任務結束統一處理
                btnStart.setEnabled(true);
                progressBar.setVisible(false);
                try {
                    get(); // 拋出執行期異常 (如有)
                    if (!isCancelled()) {
                        JOptionPane.showMessageDialog(WaitDialogDemo.this, "任務完成!");
                    }
                } catch (CancellationException ex) {
                    JOptionPane.showMessageDialog(WaitDialogDemo.this, "任務已取消");
                } catch (Exception ex) {
                    JOptionPane.showMessageDialog(WaitDialogDemo.this, "任務失敗: " + ex.getMessage(), "錯誤", JOptionPane.ERROR_MESSAGE);
                    ex.printStackTrace();
                }
            }
        };

        // 2. 執行
        worker.execute();

        // 3. 可選:顯示模態對話框阻塞用戶操作,提供取消按鈕
        showWaitDialog();
    }

    private void showWaitDialog() {
        JDialog dialog = new JDialog(this, "處理中...", true); // 模態
        dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
        dialog.setLayout(new BorderLayout(10, 10));
        dialog.add(new JLabel("正在執行任務,請稍候..."), BorderLayout.NORTH);
        JProgressBar dialogBar = new JProgressBar(0, 100);
        dialogBar.setStringPainted(true);
        dialog.add(dialogBar, BorderLayout.CENTER);
        JButton btnCancel = new JButton("取消");
        btnCancel.addActionListener(ev -> {
            worker.cancel(true); // 中斷 doInBackground
            dialog.dispose();
        });
        dialog.add(btnCancel, BorderLayout.SOUTH);
        dialog.pack();
        dialog.setLocationRelativeTo(this);
        dialog.setVisible(true); // 阻塞至此,直到 dispose
    }

    public static void main(String[] args) {
        // 確保 UI 建立在 EDT
        SwingUtilities.invokeLater(() -> new WaitDialogDemo().setVisible(true));
    }
}

四、關鍵設計模式解析

4.1 非模態 vs 模態等待

4.2 取消機制最佳實踐

  1. worker.cancel(true):設置中斷標誌,**不會強制停止執行緒**。
  2. doInBackground() 中**定期檢查 isCancelled()** 或捕獲 InterruptedException (若調用 Thread.sleep/wait/join 等可中斷方法)。
  3. 長時間運算 (無阻塞調用) 必須手動輪詢 Thread.interrupted() 或 isCancelled()。

4.3 異常處理傳遞

  • doInBackground() 拋出的異常會被包裝在 ExecutionException 中,在 done() 呼叫 get() 時重新拋出。
  • 統一在 done() try-catch ExecutionException、CancellationException、InterruptedException。

五、舊版手工執行緒管理的缺陷 (歷史參考)


// 文中原始方案:三個執行緒 (任務、對話框、銷毀監視)
// 缺陷:
// 1. 手動管理執行緒生命週期,易洩漏、死鎖
// 2. 對話框顯示/關閉時機控制複雜 (需額外監視執行緒)
// 3. 無內建取消、進度發布、異常傳遞機制
// 4. EDT 安全性完全靠開發者自覺 (invokeLater/invokeAndWait)
// 5. 程式碼冗長、難維護、難測試

六、進階替代方案

  • CompletableFuture + SwingUtilities.invokeLater:Java 8+ 函數式風格,手動切換執行緒。
  • Quasar / Project Loom (Virtual Threads):Java 21+ 虛擬執行緒,可在「類似 EDT」輕量執行緒中直接阻塞 IO,但 Swing 仍限制 UI 操作需在真實 EDT。
  • JavaFX Task/Service:若用 JavaFX,Task 是 SwingWorker 的現代對應物。

結語

Swing 介面響應性的核心在於**嚴格遵守 EDT 單執行緒規則**。SwingWorker 將「耗時邏輯移出 EDT」、「進度安全發布回 EDT」、「完成統一回調 EDT」三大核心需求封裝為成熟 API,**是實現等待提示對話框的標準、唯一推薦方案**。掌握其生命週期 (execute → doInBackground → process → done)、取消機制 (cancel/interrupted/isCancelled)、異常傳遞 (get() 拋 ExecutionException),即可輕鬆構建流暢、專業的 Swing 桌面應用體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
模式適用場景實現
非模態 (進度條嵌入主視窗)用戶可繼續操作其他功能、任務較長主視窗放置 JProgressBar,SwingWorker.process 更新
模態對話框 (阻塞用戶輸入)關鍵操作必須序列化、防止重複提交、任務較短JDialog(..., true) + setVisible(true) 阻塞,done() 中 dispose()