在開發圖形用戶界面(GUI)應用程式時,最常見的痛點莫過於「界面凍結」。當主執行緒被長時間運算佔用時,使用者點擊按鈕、拖動視窗都不會有任何反應。本文將透過 Counter1 與 Counter2 兩個經典範例,示範如何引入多線程機制,讓介面保持流暢響應。
Counter1 直接在事件處理器 actionPerformed() 中呼叫 go() 方法,後者包含一個無限迴圈:
public void go() {
while (true) {
try { Thread.currentThread().sleep(100); }
catch (InterruptedException e) {}
if (runFlag) t.setText(Integer.toString(count++));
}
}
因為 go() 永不返回,事件派發執行緒(EDT)被永久阻塞,「Start」按鈕一按下去,整個程式就卡死,唯有強制終止進程。
Counter2 的核心改動是建立 SeparateSubTask 類別繼承 Thread,把原本在 go() 裡的迴圈搬進 run():
class SeparateSubTask extends Thread {
private int count = 0;
private Counter2 c2;
private boolean runFlag = true;
public SeparateSubTask(Counter2 c2) {
this.c2 = c2;
start(); // 啟動執行緒
}
public void invertFlag() { runFlag = !runFlag; }
public void run() {
while (true) {
try { sleep(100); }
catch (InterruptedException e) {}
if (runFlag) c2.t.setText(Integer.toString(count++));
}
}
}
事件處理器只需 new SeparateSubTask(this) 即可瞬間返回,EDT 繼續服務其他事件,「Toggle」按鈕透過 invertFlag() 即時控制計數器啟停。
SeparateSubTask 與 Counter2 緊密耦合,必須持有外部類引用才能存取 TextField。利用 私有內部類 可自動捕獲外部實例,程式碼大幅簡化:
private class SeparateSubTask extends Thread {
int count = 0;
boolean runFlag = true;
SeparateSubTask() { start(); }
public void run() {
while (true) {
try { sleep(100); }
catch (InterruptedException e) {}
if (runFlag) t.setText(Integer.toString(count++)); // 直接存取外部 t
}
}
}
Counter2 參考TextField t 可改為 private,封裝性更佳invertFlag() 省略,直接 sp.runFlag = !sp.runFlagrunFlag 以 volatile 或同步機制保證可見性(範例為簡化省略)。掌握這樣「EDT 輕量化 + 背景執行緒重運算」的模式,即可打造流暢不卡頓的 Java 桌面應用。