Swing 線程不安全,採單線程設計:只能從事件派發線程(EDT)訪問將在屏幕繪製的 Swing 組件。EDT 調用 paint、update 等回調,也執行事件監聽器方法(如 ActionListener.actionPerformed)。Swing 事件驅動,在回調中更新 GUI 很自然。但若在 actionPerformed 執行耗時操作,EDT 會阻塞,界面假死,按鈕長時間保持按下狀態。此時應在獨立線程執行耗時任務,再通過 SwingUtilities 將 GUI 更新提交給 EDT。
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
GetInfoThread t = new GetInfoThread(Test.this);
t.start();
startButton.setEnabled(false); // EDT 執行,合法
}
});
class GetInfoThread extends Thread {
Test applet;
public GetInfoThread(Test applet) { this.applet = applet; }
public void run() {
while (true) {
try { Thread.sleep(500); applet.getProgressBar().setValue(Math.random() * 100); }
catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
錯誤分析:GetInfoThread 中直接設置進度條,屬非 EDT 更新組件,運行不正常。
class GetInfoThread extends Thread {
Test applet;
Runnable runx;
int value;
public GetInfoThread(final Test applet) {
this.applet = applet;
runx = new Runnable() {
public void run() {
JProgressBar jpb = applet.getProgressBar();
jpb.setValue(value);
}
};
}
public void run() {
while (true) {
try {
Thread.sleep(500);
value = (int) (Math.random() * 100);
SwingUtilities.invokeLater(runx); // 提交給 EDT,立即返回
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
invokeLater 將 Runnable 排入 EDT 隊列,隨後立即返回,不等待執行完成。
class GetInfoThread extends Thread {
Runnable getValue, setValue;
int value, currentValue;
public GetInfoThread(final Test applet) {
getValue = new Runnable() {
public void run() {
JProgressBar pb = applet.getProgressBar();
currentValue = pb.getValue();
}
};
setValue = new Runnable() {
public void run() {
JProgressBar pb = applet.getProgressBar();
pb.setValue(value);
}
};
}
public void run() {
while (true) {
try {
Thread.sleep(500);
value = (int) (Math.random() * 100);
try { SwingUtilities.invokeAndWait(getValue); } // 阻塞直到 getValue 執行完
catch (Exception ex) {}
if (currentValue != value) { SwingUtilities.invokeLater(setValue); }
} catch (Exception ex) {}
}
}
}
invokeAndWait 同樣排入 EDT 隊列,但阻塞調用線程直到 run() 執行完畢。適用於需從組件獲取信息後才能執行後續操作的場景。
invokeLater 可在 EDT 中調用;invokeAndWait 絕不可在 EDT 中調用,否則會死鎖:EDT 等待 invokeAndWait 返回才能派發事件,而 invokeAndWait 又等待 EDT 派發其 Runnable。actionPerformed 中調用 invokeAndWait 會導致 actionPerformed 永不返回,EDT 無法派發事件,死鎖發生。Swing 線程不安全,非 EDT 線程訪問組件必須透過 SwingUtilities.invokeLater(異步)或 invokeAndWait(同步)委派給 EDT 執行。選擇取決於是否需要等待更新完成並獲取返回值。