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 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  implements Runnable, Future {
    private final FutureTask computation = new Computation();

    private class Computation extends FutureTask {
        public Computation() {
            super(new Callable() {
                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() {
                    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组件 在开发时都可以得到简化

你可能感兴趣的:(Java并发)