java并发编程实战-第9章-图形用户界面应用程序
虽然GUI框架本身是单线程子系统,但应用程序可能不是单线程的,因此在编写GUI代码的时候仍然需要谨慎
地考虑线程问题
9.1 为什么GUI是单线程的?
竞态条件和死锁,
1、气泡上升和气泡下沉,导致不一致的锁顺序,引发死锁
2、mvc模式的广泛使用,mvc三者的调用顺序也会造成不一致的锁定顺序,引发死锁
9.1.1 串行事件处理
事件是例外一种类型的任务,AWT,Swing的处理机制的结构类似于Excutor
只有单个线程,来处理所有的GUI任务,处理完一个接着再处理下一个。但不利之处在于:如果某个任务执
行事件很长,则会造成程序响应性问题。
所以在启动一些执行事件较长的任务时,比如,拼写检查、文件搜索或者网络获取资源,必须重启另外一个
线程中执行,尽快把控制权交给事件线程。
9.1.2 Swing中的线程封闭机制
所有Swing(例如JButton和JTable)组件和数据模型对象(TableModel 和 TreeModel) 都封闭在事件线程
中.
swing单线程规则:The Swing single-thread rule: Swing components and models should be created,
modified, and queried only from the event-dispatching thread.
如何调度:程序清单GuiExecutor类似于Swing的invokeLater,把任务调度到事件线程中执行
9.2 短事件的GUI任务
只要任务是短期的,并且只访问GUI对象,那么基本可以忽略与线程相关的问题
9.3 长时间的GUI任务 (重点理解线程接力方式)
事件线程-提交任务到后台线程,后台线程完成后,再提交另一个任务给事件线程来更新用户界面
Listing 9.5. Long-running Task with User Feedback.
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
button.setEnabled(false);
label.setText("busy");
backgroundExec.execute(new Runnable() {
public void run() {
try {
doBigComputation();
} finally {
GuiExecutor.instance().execute(new Runnable() { //提交另一个任务给事件线程
来更新用户界面
public void run() {
button.setEnabled(true);
label.setText("idle");
}
});
}
}
});
}
});
9.3.1 取消
使用Future
Listing 9.6. Cancelling a Long-running Task.
Future<?> runningTask = null; // thread-confined
...
startButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (runningTask != null) {
runningTask = backgroundExec.submit(new Runnable() {
public void run() {
while (moreWork()) {
if (Thread.currentThread().isInterrupted()) {
cleanUpPartialWork();
break;
}
doSomeWork();
}
}
});
};
}});
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
if (runningTask != null)
runningTask.cancel(true);
}});
9.3.2 进度标志和完成标志
类似Future的取消操作,FutureTask有个done方法来响应完成事件,当后台的Callable完成后,将调用done
。
Listing 9.7. Background Task Class Supporting Cancellation, Completion Notification, and
Progress Notification.
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 forwarded to computation
}
Listing 9.8. Initiating a Long-running, Cancellable Task with BackgroundTask.
public 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;
}
public void onCompletion(boolean cancelled, String s,
Throwable exception) {
cancelButton.removeActionListener(listener);
label.setText("done");
}
};
cancelButton.addActionListener(listener);
backgroundExec.execute(task);
}
});
}
9.3.3 SwingWorker
Swing中执行长时间任务的框架,包括取消、完成通知、进度指示等。
9.4 共享数据模型
树形控件,也可以使用线程安全的树形模型来实现这个功能:通过invokerLater提交一个任务,将数据从后
台任务“推入”事件线程,或者让事件线程池通过轮询来查看是否有数据可以用。
9.4.1 线程安全的数据模型
如果数据模型支持细粒度的并发,那么时间线程和后台线程就能共享该数据模型,而不会发生响应问题。
例如第五章的 DelegatingVehicleTracker 使用ConcurrentHashMap提高并发的读写操作,但缺点在于,无
法提供一致性的数据快照。
有时候,使用版本化数据模型时,例如CopyOnWriteArrayList,虽然能提供一致性的快照,但得到的快照可
能是一个老版本了。CopyOnWriteArrayList适合在遍历操远远多于修改操作时,才能发挥更好的性能,这显
然在车辆跟踪应用程序中不适合该方法
但要实现既要提供并发访问,又要提供最新版本的数据结构并不容易。。。。。。。。
9.4.2 分解数据模型
如果程序中既包含表示的数据模型,又有程序特定的数据模型,那么该应用程序拥有一种分解模型设计
表示模型被封闭在事件线程中,其他模型,即共享模型,是线程安全的。即可以由事件线程方法,也可以由
应用程序其他线程访问
Consider a split-model design when a data model must be shared by more than one thread and
implementing a thread-safe data model would be inadvisable because of blocking, consistency,
or complexity reasons.
由于阻塞、一致性和复杂度等原因和当一个数据模型必须被多个线程共享的时候,应该考虑一个分解模型设
计而不是一个线程安全的数据模型。
9.5 其他形式的单线程子系统
例如对原生库(native libraries) 的访问等,框架参考程序清单9-1 和 9-2
Listing 9.1. Implementing SwingUtilities Using an Executor.
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);
}
}
}
使用上面的工具
Listing 9.2. Executor Built Atop SwingUtilities.
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);
}
// Plus trivial implementations of lifecycle methods
}
小结:
所有的GUI框架基本都是实现单线程的子系统。所有与表现有关的代码都作为任务在事件线程中运行。
当有一个运行时间长的任务时,提交一个任务在后台线程运行,结束后,又提交一个任务到事件线程中运行
(线程接力)。