Swing 設計之初即做出一項核心決策:不要求組件支持多線程訪問,而是採用單線程模型。所有對已實例化組件的狀態讀寫、繪製、事件處理必須在事件分發線程(EDT,Event Dispatch Thread)中執行。本文系統闡述單線程規則、例外情況、跨線程訪問 API、後台任務框架,並揭示設計哲學,助你編寫線程安全、響應流暢的 Swing 應用。
規則:一旦 Swing 組件被實例化,所有可能影響或依賴組件狀態的代碼必須在 EDT 中執行。
實例化定義:組件的 paint() 方法已被或可能被調用。頂級窗口調用 setVisible(true)、show()、pack() 時實例化;子組件隨容器實例化而實例化。
EDT 職責:執行所有繪製(paint())、事件處理(actionPerformed() 等)、佈局更新。Swing 內部將這些任務放入單一事件隊列,由 EDT 循環順序處理,保證事件順序確定性,避免多線程調度的不可測性。
This method is thread safe 的 API(極少數)。main() 中創建 JFrame、添加子組件、設置屬性),可在主線程操作。但 pack()/show() 觸發實例化後,後續操作必須切換到 EDT。init() 中構建 GUI 安全(瀏覽器保證未繪製),但不可調用 show()/setVisible(true)。repaint()、revalidate()、invalidate()、監聽器增減(addXxxListener/removeXxxListener)均線程安全。repaint/revalidate 將請求排入 EDT 隊列異步處理;invalidate 僅標記需重新佈局。非 EDT 線程(如後台計算線程、網絡回調線程)需更新 UI 時,必須將代碼提交給 EDT:
SwingUtilities.invokeLater(() -> {
label.setText("新文本"); // 在 EDT 執行
panel.revalidate();
});
立即返回,不阻塞提交線程。適用於絕大多數 UI 更新場景。
try {
SwingUtilities.invokeAndWait(() -> {
String text = textField.getText(); // 在 EDT 讀取
result[0] = text;
});
System.out.println(result[0]);
} catch (InterruptedException | InvocationTargetException e) { ... }
阻塞提交線程直到 EDT 執行完畢。風險:若提交線程持有鎖,且 EDT 執行代碼也需該鎖,將導致死鎖。原則:能用 invokeLater 絕不用 invokeAndWait。
耗時任務(大量計算、類加載、網絡/磁盤 IO)決不能在 EDT 執行,否則界面凍結、無法重繪、事件無響應。Swing 提供兩大工具類:
抽象類,需子類化實現 construct()(後台執行)與 finished()(EDT 回調結果)。注意:SwingWorker 不在 JDK 發布包中,需單獨下載(或使用 Java 6+ 引入的 javax.swing.SwingWorker 正式版)。
SwingWorker<Component, Void> worker = new SwingWorker<>() {
@Override protected Component construct() { return new ExpensiveComponent(); }
@Override protected void finished() {
try { dialog.setContentPane(get()); } catch (Exception ignore) {}
}
};
worker.start(); // 啟動後台線程
// 稍後在事件處理中
JOptionPane.showMessageDialog(frame, worker.get()); // get() 阻塞等待結果
javax.swing.Timer 的 actionPerformed 在 EDT 執行,無需 invokeLater。適用於動畫、輪詢、定時刷新。
Timer timer = new Timer(delay, e -> {
frameNumber++;
repaint(); // 安全:已在 EDT
});
timer.setCoalesce(true); // 合併多次延遲觸發
timer.start();
「線程使用具有欺騙性,雖能簡化設計,但幾乎所有情況下都極大增加調試、測試、維護難度。我們力勸三思是否絕對需要線程。」—— SubArctic 作者
setVisible(true) 後)的所有 UI 讀寫、業務邏輯均通過 invokeLater 提交 EDT。SwingWorker 或自定義線程池 + invokeLater 更新 UI。javax.swing.Timer。invokeAndWait,且確保調用線程不持有 EDT 可能需要的鎖。repaint()/revalidate() 替代手動調用 paint/validate。遵循單線程規則,善用 EDT 協作工具,你的 Swing 應用將兼具線程安全與流暢體驗。