@@@ 为了维持安全性,一些特定的任务必须运行在 Swing 的事件线程中。然而,在事件
线程中不应该执行时间较长的操作,以免用于界面失去响应。而且,由于 Swing 的数据结构
不是线程安全的,因此必须将它们限制在事件线程中。
@@@ 几乎所有的 GUI 工具包(包括 Swing 和 AWT )都被实现为单线程子系统,这意味
着所有的 GUI 操作都被限制在单个线程中。
@@@ 虽然 GUI 框架本身是单线程子系统,但应用程序可能不是单线程的,因此在编写 GUI
代码时仍然需要谨慎地考虑线程问题。
》》为什么 GUI 是单线程的
@@@ 早期的 GUI 应用程序都是单线程的,并且 GUI 事件在 “ 主事件循环 ” 进行处理。当前
的 GUI 框架则使用了一种略有不同的模型:在该模型中创建一个专门事件分发线程来处理 GUI
事件。
@@@ 单线程的 GUI 框架并不仅限于在 Java 中,在 Qt 、 NexiStep 、 MacOS 、 Cocoa 、
X Windows 以及其他环境中的 GUI 框架都是单线程的。
@@@ GUI 中的单线程的事件队列模型:采用一个专门的线程从队列中抽取事件,并将它们转发
到应用程序定义的事件处理器。
@@@ 在多线程的 GUI 框架中更容易发生死锁问题,其部分原因在于,在输入事件的处理
过程与GUI 组件的面向对象模型之间会存在错误的交互。
@@@ 另一个在多线程 GUI 框架中导致死锁的原因就是 “ 模型----视图----控制器(MVC)” 这种
设计模式的广泛使用。
---------- “ 控制 ” 模块将调用“ 模型 ”模块,而“ 模型 ” 模块将发生的变化通知给 “ 视图 ” 模块。
“ 控制 ” 模块同样可以调用“ 视图 ” 模块,并调用 “ 模型 ” 模块来查询模块的状态。这将再次
导致不一致的锁定顺序并出现死锁。
@@@ 单线程的 GUI 框架通过线程封闭机制来实现线程安全性。所有 GUI 对象,包括可视化组件
和数据模型等,都只能在事件线程中访问。
### 串行事件处理
@@@ GUI 应用程序需要处理一些细粒度的事件,例如点击鼠标 、 按下键盘或定时器超时
等。事件是另一种类型的任务,而 AWT 和 Swing 提供的事件处理机制在结构上也类似于 Executor 。
### Swing 中的线程封闭机制
@@@ 所有 Swing 组件(例如 JButton 和 JTable)和数据模型对象(例如 TableModel 和
TreeModel )都被封闭在事件线程中,因此任何访问它们的代码都必须在事件线程中运行。
@@@ Swing 的单线程规则是: Swing 中的组件以及模型只能在这个事件分发线程中进行创建 、
修改以及查询。
@@@ 在 Swing 中只有少数方法可以安全的从其他线程中调用,而在 Javadoc 中已经很清楚地
说明了这些方法的线程安全性。
@@@ 单线程规则的其他一些例外情况包括:
---------- SwingUtilities.isEventDispatchThread : 用于判断当前线程是否是事件线程。
---------- SwingUtilities.invokeLater : 该方法可以将一个 Runnable 任务调度到事件线程中执行
(可以从任意线程中调用)
---------- SwingUtilities.invokeAndWait :该方法可以将一个 Runnable 任务调度事件线程中执行,
并且阻塞当前线程直到任务完成(只能从非 GUI 线程中调用)
---------- 所有将重绘(Repaint)请求或重生效(Revalidation)请求插入队列的方法(可以从任意
线程中调用)
---------- 所有添加或移除监听器的方法(这些方法可以从任意线程中调用,但监听器本身一定要
在事件线程中调用)
@@@ 可以将 Swing 的事件线程视为一个单线程的 Executor ,它处理来自事件队列的任务。
与线程池一样,有时候工作者线程会死亡并由另一个新线程来替代,但这一切要对任务透明。
》》短时间的 GUI 任务
@@@ 在 GUI 应用程序中,事件在事件线程中产生,并通过 “ 气泡上升 ” 的方式传递给应用程序
提供的监听器,而监听器则根据收到的时间执行一些计算来修改表现对象。
@@@ Swing 将大多数可视化组件都分为两个对象,即模型对象与视图对象。
----------- 在模型对象中保存的是将被显示的数据,而在视图对象中则保存了控制显示方式的规则。
----------- 模型对象可以通过引发事件来表示数据模型发生了变化,而视图对象则通过 “ 订阅” 来
接收这些事件。
----------- 当视图对象收到表示模型数据已发生变化的事件时,将向模型对象查询更新的数据,并
更新界面显示。
@@@ Swing 数据模型的 fireXxx 方法通常会直接调用模型监听器,而不会向线程队列中提交新
的事件,因此 fireXxx 方法只能从事件线程中调用。
》》长时间的 GUI 任务
@@@ 如果所有任务的执行时间都较短(并且应用程序中不包含执行时间较长的非 GUI 部分),
那么整个应用程序都可以在事件线程内部运行,并且完全不用关心线程。
@@@ 可以创建自己的 Executor 来执行时间较长的任务。对于长时间的任务,可以使用缓存
线程池。
@@@ 理解什么是后台线程,什么是事件线程。
-------- 在 GUI 应用程序中,“ 线程接力 ” 是处理长时间任务的典型方法。
### 取消
@@@ 当某个任务在线程中运行了过长时间还没有结束时,用户可能希望取消它。你可以直接
通过线程中断来实现取消操作,但是一种更简单的办法是使用 Future , 专门用来管理可取消
的任务。
如果调用 Future 的 cancel 方法,并将参数 mayInterruptIfRunning 设置为 true ,那么
这个 Future 可以中断正在执行任务的线程。如果你编写的任务能够响应中断,那么当它被取消
时就可以提前返回。
### 进度标识和完成标识
@@@ 通过 Future 来表示一个长时间的任务,可以极大地简化取消操作的实现。在 FutureTask
中也有一个 done 方法同样有助于实现完成通知。当后台的 Callable 完成后,将调用 done 。
通过 done 方法在事件线程中触发一个完成任务。
@@@ 在事件线程中调用 onProgress , 从而更新用户界面以显示可视化的进度信息。
### SwingWorker
@@@ 通过 FutureTask 和 Executor 构建一个简单的框架,它会在后台线程中执行长时间的
任务,因此不会影响 GUI 的响应性。在任何单线程的 GUI 框架都可以使用这些技术,而不仅
限于 Swing 。
》》共享数据模型
@@@ Swing 的表现对象(包括 TableModel 和 TreeModel 等数据模型)都被封闭在事件
线程中。
@@@ 要在程序中强制实施单线程规则是很容易的:不要从主线程中访问数据模型或
表现组件。
@@@ 数据模型中的数据由用户来输入或者由应用程序在启动时静态地从文件或其他数据源
加载。
但是在某些情况下,表现模型对象只是一个数据源(例如数据库 、 文件系统或远程
服务等)的视图对象。
### 线程安全的数据模型
@@@ 只要阻塞操作不会过度地影响响应性,那么多个线程操作同一份数据的问题都可以
通过线程安全的数据模型来解决。如果数据模型支持细粒度的并发,那么事件线程和后台线程
就能共享该数据模型,而不会发生响应性问题。
@@@ 线程安全的数据模型必须在更新模板时产生事件,这样视图才能在数据发生变化后
进行更行。
@@@ 要构建一个既能提供高效的并发访问又能在旧数据无效后不再维护它们的数据结构
并不容易。
### 分解数据模型
@@@ 从 GUI 的角度看,Swing 的表格模型类,例如 TableModel 和 TreeModel ,都是保存
将要显示的数据的正式方法。然而,这些模型对象本身通常都是应用程序中其他对象的 “ 视图 ”。
如果在程序中既包含用于表示的数据模型,又包含应用程序特定的数据模型,那么这种应用程序
就被称为拥有一种分解模型设计。
@@@ 在分解模型设计中,有表现模型和其他模型。
---------- 表现模型
*** 表现模型被封闭在事件线程中
*** 表现模型会注册共享模型的监听器,从而在更新时得到通知。
---------- 其他模型(共享模型)
*** 共享模型是线程安全的,因此既可以由事件线程方法,也可以由应用程序线程访问。
补充:
表现模型可以在共享模型中得到更新:通过将相关状态的快照嵌入到更新消息中,
或者由表现模型在收到更新事件时直接从共享模型中获取数据。
@@@ 在更新数据时,使用的方法:
----------发送一个完成的快照
---------- 使用增量更新信息(这种方法是更高效的)
*** 增量更新信息将共享模型上的更新操作序列化,并在事件线程中重现。
*** 增量更新信息的另一个好处是:细粒度的变化信息可以提高显示的视觉效果-----
如果只有一辆移动,那么只需要更新发生变化的区域,而不用重绘整个显示图形。
@@@ 如果一个数据模型必须被多个线程共享,而且由于阻塞 、 一致性或复杂度等原因而
无法实现一个线程安全的模型时,可以考虑使用分解模型设计。
》》其他形式的单线程子系统
@@@ 线程封闭不仅仅可以在 GUI 中使用,每当某个工具需要被实现为单线程子系统时,
都可以使用这项技术。
》》小结
@@@ 所有 GUI 框架基本上都实现为单线程的子系统,其中所有与表现相关的代码都作为
任务在事件线程中运行。由于只有一个事件线程,因此运行时间较长的任务会降低 GUI 程序
的响应性,所以应该放在后台线程中运行。
@@@ 辅助类: SwingWorker