Java併發編程實戰 圖形用戶界面應用程序總結

爲什麼GUI是單線程的
許多人曾經嘗試過編寫多線程的GUI框架 但最終都由於競態條件和死鎖導致的穩定性問題而又重新回到單線程的事件隊列模型:採用一個專門的線程從隊列中抽取事件 並將它們轉發到應用程序定義的事件處理器(AWT最初嘗試在更大程度上支持多線程訪問 而正是基於在AWT中得到的經驗和教訓 Swing在實現時決定採用單線程模型)

不過 我相信你還是可以成功地編寫出多線程的GUI工具包 只要做到:非常謹慎地設計多線程GUI工具包 詳盡無遺地公開工具包的鎖定方法 以及你非常聰明 非常仔細 並且對工具包的整體結構有着全局理解 然而 如果在上述某個方面稍有偏差 那麼即使程序在大多數時候都能正確運行 但在偶爾情況下仍會出現(死鎖引起的)掛起或者(競爭引起的)運行故障 只有那些深入參與工具包設計的人們才能夠正確地使用這種多線程的GUI框架
然而 我並不認爲這些特性能夠在商業產品中得到廣泛使用 可能出現的情況是:大多數普通的程序員發現應用程序無法可靠地運行 而又找不出其中的原因 於是 這些程序員將感到非常不滿 並詛咒這些無辜的工具包

單線程的GUI框架通過線程封閉機制來實現線程安全性 所有GUI對象 包括可視化組件和數據模型等 都只能在事件線程中訪問 當然 這只是將確保線程安全性的一部分工作交給應用程序的開發人員來負責 他們必須確保這些對象被正確地封閉在事件線程中

串行事件處理
串行任務處理不利之處在於 如果某個任務的執行時間很長 那麼其他任務必須等到該任務執行結束

Swing中的線程封閉機制
Swing的單線程規則是:Swing中的組件以及模型只能在這個事件分發線程中進行創建 修改以及查詢

使用Executor來實現SwingUtilities

public class SwingUtilities {
    private static final ExecutorService exec =
            Executors.newSingleThreadExecutor(new SwingThreadFactory());
    private static volatile Thread swingThread;

    private static class SwingThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            swingThread = new Thread(r);
            return swingThread;
        }
    }

    public static boolean isEventDispatchThread() {
        return Thread.currentThread() == swingThread;
    }

    public static void invokeLater(Runnable task) {
        exec.execute(task);
    }

    public static void invokeAndWait(Runnable task)
            throws InterruptedException, InvocationTargetException {
        Future f = exec.submit(task);
        try {
            f.get();
        } catch (ExecutionException e) {
            throw new InvocationTargetException(e);
        }
    }
}

這並非SwingUtilities的真實實現 因爲Swing的出現時間要早於Executor框架 但如果現在來實現Swing 或許應該採用這種實現方式

基於SwingUtilities構建的Executor

public class GuiExecutor extends AbstractExecutorService {
    // Singletons have a private constructor and a public factory
    private static final GuiExecutor instance = new GuiExecutor();

    private GuiExecutor() {
    }

    public static GuiExecutor instance() {
        return instance;
    }

    public void execute(Runnable r) {
        if (SwingUtilities.isEventDispatchThread())
            r.run();
        else
            SwingUtilities.invokeLater(r);
    }

    public void shutdown() {
        throw new UnsupportedOperationException();
    }

    public List<Runnable> shutdownNow() {
        throw new UnsupportedOperationException();
    }

    public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    public boolean isShutdown() {
        return false;
    }

    public boolean isTerminated() {
        return false;
    }
}

短時間的GUI任務
在GUI應用程序中 事件在事件線程中產生 並通過 氣泡上升 的方式傳遞給應用程序提供的監聽器 而監聽器則根據收到的時間執行一些計算來修改表現對象 爲了簡便 短時間的任務可以把整個操作都放在事件線程中執行 而對於長時間的任務 則應該將某些操作放到另一個線程中執行

簡單的事件監聽器

	private final JButton colorButton = new JButton("Change color");
    private final Random random = new Random();

    private void backgroundRandom() {
        colorButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                colorButton.setBackground(new Color(random.nextInt()));
            }
        });
    }

只要任務是短期的 並且只訪問GUI對象(或者其他線程封閉或線程安全的應用程序對象) 那麼就可以基本忽略與線程相關的問題 而在事件線程中可以執行任何操作都不會出問題

長時間的GUI任務
在複雜的GUI應用程序中可能包含一些執行時間較長的任務 並且可能超過了用戶可以等待的時間 例如拼寫檢查 後臺編輯或者獲取遠程資源等 這些任務必須在另一個線程中運行 才能使得GUI在運行時保持高響應性

將一個長時間任務綁定到一個可視化組件

private static ExecutorService exec = Executors.newCachedThreadPool();
...
private final JButton computeButton = new JButton("Big computation");

    private void longRunningTask() {
        computeButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                exec.execute(new Runnable() {
                    public void run() {
                        /* Do big computation */
                    }
                });
            }
        });
    }

支持用戶反饋的長時間任務

private final JButton button = new JButton("Do");
    private final JLabel label = new JLabel("idle");

    private void longRunningTaskWithFeedback() {
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                button.setEnabled(false);
                label.setText("busy");
                exec.execute(new Runnable() {
                    public void run() {
                        try {
                            /* Do big computation */
                        } finally {
                            GuiExecutor.instance().execute(new Runnable() {
                                public void run() {
                                    button.setEnabled(true);
                                    label.setText("idle");
                                }
                            });
                        }
                    }
                });
            }
        });
    }

在按下按鈕時觸發的任務中包含3個連續的子任務 它們將在事件線程與後臺線程之間交替運行 第一個子任務更新用戶界面 表示一個長時間的操作已經開始 然後在後臺線程中啓動第二個子任務 當第二個子任務完成時 它把第三個子任務再次提交到事件線程中運行 第三個子任務也會更新用戶界面來表示操作已經完成 在GUI應用程序中 這種 線程接力 是處理長時間任務的典型方法

取消
當某個任務在線程中運行了過長時間還沒有結束時 用戶可能希望取消它 你可以直接通過線程中斷來實現取消操作 但是一種更簡單的辦法是使用Future 專門用來管理可取消的任務

取消一個長時間任務

private final JButton startButton = new JButton("Start");
    private final JButton cancelButton = new JButton("Cancel");
    private Future<?> runningTask = null; // thread-confined

    private void taskWithCancellation() {
        startButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (runningTask != null) {
                    runningTask = exec.submit(new Runnable() {
                        public void run() {
                            while (moreWork()) {
                                if (Thread.currentThread().isInterrupted()) {
                                    cleanUpPartialWork();
                                    break;
                                }
                                doSomeWork();
                            }
                        }

                        private boolean moreWork() {
                            return false;
                        }

                        private void cleanUpPartialWork() {
                        }

                        private void doSomeWork() {
                        }

                    });
                }
                ;
            }
        });

        cancelButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                if (runningTask != null)
                    runningTask.cancel(true);
            }
        });
    }

輪詢線程的中斷狀態 並且在發現中斷時提前返回

進度標識和完成標識
通過Future來表示一個長時間的任務 可以極大地簡化取消操作的實現 在FutureTask中也有一個done方法同樣有助於實現完成通知 當後臺的Callable完成後 將調用done

支持取消 完成通知以及進度通知的後臺任務類

public abstract class BackgroundTask <V> implements Runnable, Future<V> {
    private final FutureTask<V> computation = new Computation();

    private class Computation extends FutureTask<V> {
        public Computation() {
            super(new Callable<V>() {
                public V call() throws Exception {
                    return BackgroundTask.this.compute();
                }
            });
        }

        protected final void done() {
            GuiExecutor.instance().execute(new Runnable() {
                public void run() {
                    V value = null;
                    Throwable thrown = null;
                    boolean cancelled = false;
                    try {
                        value = get();
                    } catch (ExecutionException e) {
                        thrown = e.getCause();
                    } catch (CancellationException e) {
                        cancelled = true;
                    } catch (InterruptedException consumed) {
                    } finally {
                        onCompletion(value, thrown, cancelled);
                    }
                };
            });
        }
    }

    protected void setProgress(final int current, final int max) {
        GuiExecutor.instance().execute(new Runnable() {
            public void run() {
                onProgress(current, max);
            }
        });
    }

    // Called in the background thread
    protected abstract V compute() throws Exception;

    // Called in the event thread
    protected void onCompletion(V result, Throwable exception,
                                boolean cancelled) {
    }

    protected void onProgress(int current, int max) {
    }

    // Other Future methods just forwarded to computation
    public boolean cancel(boolean mayInterruptIfRunning) {
        return computation.cancel(mayInterruptIfRunning);
    }

    public V get() throws InterruptedException, ExecutionException {
        return computation.get();
    }

    public V get(long timeout, TimeUnit unit)
            throws InterruptedException,
            ExecutionException,
            TimeoutException {
        return computation.get(timeout, unit);
    }

    public boolean isCancelled() {
        return computation.isCancelled();
    }

    public boolean isDone() {
        return computation.isDone();
    }

    public void run() {
        computation.run();
    }
}

基於FutureTask構造的BackgroundTask還能簡化取消操作 Compute不會檢查線程的中斷狀態 而是調用Future.isCancelled

通過BackgroundTask來執行長時間的並且可取消的任務

private void runInBackground(final Runnable task) {
        startButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                class CancelListener implements ActionListener {
                    BackgroundTask<?> task;
                    public void actionPerformed(ActionEvent event) {
                        if (task != null)
                            task.cancel(true);
                    }
                }
                final CancelListener listener = new CancelListener();
                listener.task = new BackgroundTask<Void>() {
                    public Void compute() {
                        while (moreWork() && !isCancelled())
                            doSomeWork();
                        return null;
                    }

                    private boolean moreWork() {
                        return false;
                    }

                    private void doSomeWork() {
                    }

                    public void onCompletion(boolean cancelled, String s, Throwable exception) {
                        cancelButton.removeActionListener(listener);
                        label.setText("done");
                    }
                };
                cancelButton.addActionListener(listener);
                exec.execute(task);
            }
        });
    }

SwingWorker
我們已經通過FutureTask和Executor構建了一個簡單的框架 它會在後臺線程中執行長時間的任務 因此不會影響GUI的響應性 在任何單線程的GUI框架都可以使用這些技術 而不僅限於Swing 在Swing中 這裏給出的許多特性是由SwingWorker類提供的 包括取消 完成通知 進度指示等

共享數據模型
Swing的表現對象(包括TableModel和TreeModel等數據模型) 都被封閉在事件線程中 在簡單的GUI程序中 所有的可變狀態都被保存在表現對象中 並且除了事件線程之外 唯一的線程就是主線程 要在這些程序中強制實施單線程規則是很容易的:不要從主線程中訪問數據模型或表現組件 在一些更復雜的程序中 可能會使用其他線程對持久化的存儲(例如文件系統 數據庫等)進行讀寫操作以免降低系統的響應性

線程安全的數據模型
只要阻塞操作不會過度地影響響應性 那麼多個線程操作同一份數據的問題都可以通過線程安全的數據模型來解決 如果數據模型支持細粒度的併發 那麼事件線程和後臺線程就能共享該數據模型 而不會發生響應性問題 線程安全的數據模型必須在更新模板時產生事件 這樣視圖才能在數據發生變化後進行更新

分解數據模型
從GUI的角度看 Swing的表格模型類 例如TableModel和TreeModel 都是保存將要顯示的數據的正式方法 然而 這些模型對象本身通常都是應用程序中其他對象的 視圖 如果在程序中既包含用於表示的數據模型 又包含應用程序特定的數據模型 那麼這種應用程序就被稱爲擁有一種分解模型設計
在分解模型設計中 表現模型被封閉在事件線程中 而其他模型 即共享模型 是線程安全的 因此既可以由事件線程方法 也可以由應用程序線程訪問 表現模型會註冊共享模型的監聽器 從而在更新時得到通知 然後 表示模型可以在共享模型中得到更新:通過將相關狀態的快照嵌入到更新消息中 或者由表現模型在收到更新事件時直接從共享模型中獲取數據
快照這種方法雖然簡單 但卻存在着一些侷限 當數據模型很小 更新頻率不高 並且這兩個模型的結構相似時 它可以工作得良好 如果數據模型很大 或者更新頻率極高 在分解模型包含的信息中有一方或雙方對另一方不可見 那麼更高效的方式是發送增量更新信息而不是發送一個完整的快照 這種方法將共享模型上的更新操作序列化 並在事件線程中重現 增量更新的另一個好處是 細粒度的變化信息可以提高顯示的視覺效果 如果只有一輛車移動 那麼只需更新發生變化的區域 而不用重繪整個顯示圖形

如果一個數據模型必須被多個線程共享 而且由於阻塞 一致性或複雜度等原因而無法實現一個線程安全的模型時 可以考慮使用分解模型設計

其他形式的單線程子系統
線程封閉不僅僅可以在GUI中使用 每當某個工具需要被實現爲單線程子系統時 都可以使用這項技術 有時候 當程序員無法避免同步或死鎖等問題時 也將不得不使用線程封閉 例如 一些原生庫(Native Library)要求:所有對庫的訪問 甚至當通過System.loadLibrary來加載庫時 都必須放在同一個線程中執行

小結
所有GUI框架基本上都實現爲單線程的子系統 其中所有與表現相關的代碼都作爲任務在事件線程中運行 由於只有一個事件線程 因此運行時間較長的任務會降低GUI程序的響應性 所以應該放在後臺線程中運行 在一些輔助類中提供了對取消 進度指示以及完成指示的支持 因此對於執行時間較長的任務來說 無論在任務中包含了GUI組件還是非GUI組件 在開發時都可以得到簡化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章