[关闭]
@king 2015-01-11T21:22:36.000000Z 字数 15251 阅读 4005

Swing多线程

Java



深入浅出Swing事件派线程

《FilthyRichClients》:EventQueue的派发机制由单独的一个线程管理,这个线程称为事件派发线程(EDT)

  1. public class SyncQueue {
  2. private List buffer = new ArrayList();
  3. public synchronized Object pop() {
  4. Object e;
  5. while (buffer.size() == 0) {
  6. try {
  7. wait();
  8. } catch (InterruptedException e1) {
  9. // ignore it
  10. }
  11. }
  12. e = buffer.remove(0);
  13. return e;
  14. }
  15. public synchronized void push(Object e) {
  16. notifyAll();
  17. buffer.add(e);
  18. }
  19. }

而Swing的事件队列就类似(基本原理相似,但是Swing内部实现会做些优化)于上述的事件队列,说它是单线程图形工具包指的是仅有单一消费者,也就 是常说的事件分发线程(EDT),一般来讲,除非你的应用程序停止,否则EDT会永不间断地徘徊在处理请求与等待请求之间。

对于单一线程的事件队列来说有两个非常突出的特性:

  • 将同步操作转为异步操作。
  • 将并行处理转换为串行顺序处理。

EDT要处理所有GUI操作,它是职责分明且非常忙碌的。也就是说你要记住两条原则:

  • 职责分明,任何GUI请求都应该在EDT中调用。
  • 需要处理的GUI请求非常多,包括窗口移动、组件自动重绘、刷新,它很忙,所以任何与GUI无关的处理不要由EDT来负责,尤其是I/O这种耗时的操作。

Swing不是一个线程安全的API,为什么要这样设计:Swing的线程安全不是靠自身组件的API来保障,虽然repaint方法是这样,但是大多数Swing API是非线程安全的,也就是说不能在任意地方调用,它应该只在EDT中调用。Swing的线程安全靠事件队列和EDT来保障。

invokeLater和invokeAndWait:Swing自身不是线程安全,对非EDT的并发调用需通过 invokeLater(runnable)和invokeAndWait(runnable)使请求插入到队列中等待EDT去执行。

  • invokeLater(runnable)方法是异步的,它会立即返回,具体何时执行请求并不确定,所以命名invokeLater是稍后调用。
  • invokeAndWait(runnable)方法是同步的,它被调用结束会立即block当前线程(调用invokeAndWait的那个线程)直到EDT处理完那个请求。invokeAndWait一般的应用是取得Swing组件的数据。

例如取得JSlider组件的当前值:

  1. public class Task implements Runnable {
  2. private JSlider slider;
  3. private int value;
  4. public Task() {
  5. //slider = ...;
  6. }
  7. @Override
  8. public void run() {
  9. try {
  10. Thread.sleep(1000); // 有意停住1秒
  11. } catch (InterruptedException e) {}
  12. value = slider.getValue();
  13. }
  14. public int getValue() {
  15. return value;
  16. }
  17. }

而外部非EDT线程可以这样调用:

  1. Task task = new Task();
  2. try {
  3. EventQueue.invokeAndWait(task);
  4. } catch (InterruptedException e) {
  5. } catch (InvocationTargetException e) {
  6. }
  7. int value = task.getValue();

当线程运行到EventQueue.invokeAndWait(task)时会立即被block至少1秒,待invokeAndWait返回时已经可以 安全地取到值了。invokeAndWait被这样命名也反映了使用的意图:调用并等待结果。invokeAndWait有非常重要的一条准则是它不能在 EDT中被调用,否则程序会抛出Error,请求也不会去执行。
为什么要有这样一条限制?结合前文不难得出:防止死锁。如果invokeAndWait在EDT中调用,那么首先将请求压进队列,然后EDT便被 block(因为它就是调用invokeAndWait的当前线程)等待请求结束通知它继续运行,而实际上请求将永远得不到执行,因为它在等待队列的调度 使EDT执行它,这就陷入一个僵局-EDT等待请求先执行,请求又等待EDT对队列的调度。彼此等待对方释放锁是造成死锁的四类条件之一。Swing有意 地避免了这类情况的发生。

作为队列,一条基本原则就是先进先出。那么paintImmediately到底是怎样的呢?显然这个调用请求不会稍后去 执行,也就是说不会插入到队列的末尾等到排在它前面的请求执行完再去执行它,而是“破坏”顺序性原则优先去执行,前面提到,Swing的事件队列相对基础 的同步队列做了很多优化,那么这么说它是否被插入到队列最前面呢,也就是0这个位置?貌似也不是,书上说“已经在EDT中调用的方法中间...”,那么就 是比当前正在处理的绘制请求还要优先,因为它是当前绘制请求的一部分,所以当前绘制请求(EDT正在处理的那个请求)要等它处理完成后再继续处理。(好好 体会吧)

事件队列是一个非常好的处理并发设计模型,不仅 Swing用它来处理后台,Java的很多地方都在用,只不过对于处理服务器端的并发请求有多个处理线程在等候处理请求,也就是常说的线程池。而对于单用 户的桌面应用,单线程调用要比多现成API更简单,“Swing后台这样做是为了保证事件的顺序和可预见性”,而且相对于服务器,客户端桌面层的请求要少得多,所以单线程就足够应对了。


Java6印象:桌面应用胜出者


开发效率

新的Java编译器API允许在Java应用程序中编译Java代码。在编译的时候,应用程序能够访问格式化的类库依赖信息、警告信息、错误信息以及编译中产生的其他消息(该功能已经被netbeans6.0集成进去,实现新的Javac API,并籍此提高错误信息的提示)。尽管此项功能我不会经常用到,但是我已经发现它的新用处了。比如,用它为应用程序数据库迅速生成数据访问层。我写的代码生成并编译访问应用程序数据库的类,最终生成jar文件,这些jar文件产生、生成之后作为系统的一部分部署到Ant脚本中。在应用程序中生成并编译使得代码生成变得可以互动起来,我可以不断修改并反复生成这些类。
为使用Java的脚本功能,Java6支持了JSR223,JSR223提供脚本语言访问Java内部的框架,你可以在运行时定位并启动脚本引擎来运行你指定的脚本。另外,Web脚本框架还允许脚本在任何Servlet容器内生成Web页面。
对于调试来说,Java平台调试接口(JPDA)增强了检测死锁以及为被锁定监控对象产生堆栈跟踪的功能。此外,Java6添加了允许客户程序attach到一个正在运行的虚拟机进行诊断的功能。


桌面功能和改进


启动画面支持

启动画面告诉用户等待应用程序启动。Java6甚至提供了在虚拟机启动之前展现启动画面的支持。


Java基础类(JFC)以及Swing改进

Java6调整了Windows API,使Swing既提高了运行速度,又保证了当前Windows版本的Windows外观。
增强了布局管理器,加入了自定义布局管理器以及其他简化界面组件的布局器。
极大的增强了Swing的drag-and-drop功能,使其更加灵活。
真双缓冲机制提供了快速平滑的界面切换。
系统托盘支持,在java.awt中增添了两个新类SystemTray和TrayIcon,允许你在Windows和Gnome Linux系统托盘上添加图标、tool tips、以及弹出菜单。系统托盘是所有应用程序共享的桌面领域,通常位于桌面的左下角,动作事件允许Java应用程序跟踪你放入托盘上图标鼠标点击事件。我发现这个新功能对我的服务器端程序也有用,比如和下文所提到的Desktop API结合使用能容易地为应用程序管理员启动管理页面浏览器。不管是什么操作系统(Linux还是Windows),我不再需要记住应用程序管理端口或者URL,只需要点击图标,页面就出现了。
JTable的增强打印的支持。
Java2D的增强:增强了文本显示质量,特别是在液晶(LCD)显示器上,同本地桌面字体反走样设置集成确保了文本显示的一致性。
新的java.awt.Desktop的API:Java6新的Desktop包目的是使Java用户界面程序成为一等公民。使用该包,Java应用程序能够启动缺省的浏览器和电子邮件客户端,并且和普通桌面应用程序(比如OpenOffice)集成,能够打开、编辑并且打印特定类型的文件。Desktop包通过动作事件(Desktop.Action)来提供此项功能,让你能集成到你的应用系统中。


单线程模型

单线成模型对于事件处理不保证线程安全性(Thread Safety),所有的事件处理都在Event Dispatch Thread(EDT)上进行,此一类事件模型通常叫做单线程模型。这种模型规定所有对组件的访问操作必须在EDT上完成。为什么对于组件的访问需要在EDT上完成?这主要是为了保证对于组件状态的改变是同步的,保证了界面组件的可确定性。这中模型是大部分图形用户界面工具采用的模型,包括Swing/AWT、SWT、GTK、WinForm等等。
这种模型的好处是,结构设计和代码实现都比较简单,避免了为了实现线程同步的复杂处理。但是也带来了一些问题,最常见的问题是,程序员容易将长时间复杂任务的处理放在事件处理函数完成,造成EDT线程被阻塞,给用户造成界面失去响应的错觉。其实人们对于Swing速度慢和反映迟钝的感觉大部分来源于此,简单的说,是程序员的问题,而不是Swing自身的问题,是因为程序员没有理解这种事件处理机制造成的。其实在SWT、GTK、WinForm等任何以这种事件模型为基础的工具都会出现。重现的方法就是你简单的将长时间处理的任务放在事件处理函数中,你的用户界面就会失去响应。
如何解决这种问题?通用的办法就是采用异步线程处理长时间任务。但是还要记住的是,在这种任务中对于界面的更新要采用SwingUtilities.invokeLater或者在SWT采用Synchronize方法,将访问操作放到EDT上进行。

AWT是各个平台所有组件集合的交集,而Swing和SWT则是各个平台组件的并集。


Swing框架之Component

Swing的事件处理过程为:事件调度线程(Event Dispatch Thread)从事件队列(EventQueue)中获取底层系统捕获的原生事件,如鼠标、键盘、焦点、PAINT事件等。接着调用该事件源组件的dispachEvent。该方法过滤出特殊事件后,调用processEvent进行处理。processEvent方法根据事件类型调用注册在这个组件上的相应事件处理器函数。事件处理器函数根据这些事件的特征,判断出用户的期望行为,然后根据期望行为改变组件的状态,然后根据需要刷新组件外观,触发带有特定语义的高级事件。此事件继续传播下去,直至调用应用程序注册在该组件上的处理器函数。
Pump an Event->Dispatch & Process Event->MouseListener.mousePressed->fireActionPerformed->ActionListener.actionPeformed->Do database query and display result to a table->Return from actionPerformed->Return from fireActionPerformed->Return from MouseListener.mousePressed->Pump another Event.
事件调度线程在应用程序事件处理函数actionPerformed没有完成之前是不能处理下一个事件的,如果应用程序处理函数是一个时间复杂的任务(比如查询数据库并将结果显示到表格中),后面包括PAINT事件将在长时间内得不到执行。由于PAINT事件负责将界面更新,所以这就使用户界面失去响应。
不了解Swing的这种事件处理模型的人往往将时间复杂的任务放在处理函数中完成,这是造成Swing应用程序速度很慢的原因。用户触发这个动作,用户界面就失去了响应,于是给用户的感觉就是Swing太慢了。其实这个错误是程序员造成的,并不是Swing的过失。
那么如何避免这个问题,编写响应速度快的Swing应用程序呢?在SwingWorker的javadoc中有这样两条原则:

  • Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the application becomes unresponsive. 耗时任务不要放到事件调度线程上执行,否则程序就会失去响应。
  • Swing components should be accessed on the Event Dispatch Thread only. Swing组件只能在事件调度线程上访问。

因此处理耗时任务时,首先要启动一个专门线程,将当前任务交给这个线程处理,而当前处理函数立即返回,继续处理后面未决的事件。 其次,在为耗时任务启动的线程访问Swing组件时,要使用SwingUtilties. invokeLater或者SwingUtilities.invokeAndWait来访问,invokeLater和invokeAndWait的参数都是一个Runnable对象,这个Runnable对象将被像普通事件处理函数一样在事件调度线程上执行。

  1. void myButton_actionPerformed(ActionEvent evt){
  2. new MyQueryTask().start();
  3. }
  4. public class MyQueryTask extends Thread{
  5. public void run(){
  6. //查询数据库
  7. final ResultSet result=...;
  8. //显示记录
  9. for(;result.next();){
  10. //往表的Model中添加一行数据,并更新进度条,注意这都是访问组件
  11. SwingUtilities.invokeLater(new Runnable(){
  12. public void run(){
  13. addRecord(result);
  14. }
  15. });
  16. }
  17. ....
  18. }
  19. void addRecord(ResultSet result){
  20. //往表格中添加数据
  21. jTable.add....
  22. //更新进度条
  23. jProgress.setValue(....);
  24. }
  25. }

JDK 1.6以后,Swing提供了一个专门的类SwingWorker能帮你解决这个编程范式,你所需要做的就是继承这个类,重载doInBackground,然后在actionPeformed中调用它的execute方法,并通过publish/process方法来更新界面。

很容易将上面的例子改成SwingWorker的版本:

  1. void myButton_actionPerformed(ActionEvent evt){
  2. new MyQueryTask().execute();
  3. }
  4. public class MyQueryTask extends SwingWorker{
  5. public void doInBackground(){
  6. //查询数据库
  7. final ResultSet result=...;
  8. //显示记录
  9. for(;result.next();){
  10. //往表的Model中添加一行数据,并更新进度条,注意这都是访问组件
  11. publish(result); //将数据块发送给 process(java.util.List) 方法
  12. }
  13. ....
  14. }
  15. //在事件指派线程 上异步地从 publish 方法接收数据块。
  16. public void process(Object ... result){
  17. //往表格中添加数据
  18. jTable.add....
  19. //更新进度条
  20. jProgress.setValue(....);
  21. }
  22. }

API 中的SwingWorker例子
假定想找到“Meaning of Life”并在 JLabel 中显示结果。

  1. final JLabel label;
  2. class MeaningOfLifeFinder extends SwingWorker<String, Object> {
  3. @Override
  4. public String doInBackground() {
  5. return findTheMeaningOfLife();
  6. }
  7. //doInBackground 方法完成后,在事件指派线程 上执行此方法。
  8. @Override
  9. protected void done() {
  10. try {
  11. label.setText(get());
  12. } catch (Exception ignore) {}
  13. }
  14. }
  15. (new MeaningOfLifeFinder()).execute();

在希望处理已经在事件指派线程 上准备好的数据时,下一个例子很有用。

现在想要查找第一个 N 素数值并在 JTextArea 中显示结果。在计算过程中,想在 JProgressBar 中更新进度。最后,还要将该素数值打印到 System.out。

  1. class PrimeNumbersTask extends SwingWorker<List<Integer>, Integer> {
  2. PrimeNumbersTask(JTextArea textArea, int numbersToFind) {
  3. //initialize
  4. }
  5. @Override
  6. public List<Integer> doInBackground() {
  7. while (! enough && ! isCancelled()) {
  8. number = nextPrimeNumber();
  9. publish(number); //将数据块发送给 process(java.util.List) 方法。
  10. setProgress(100 * numbers.size() / numbersToFind); //设置 progress 绑定属性
  11. }
  12. return numbers;
  13. }
  14. //在事件指派线程 上异步地从 publish 方法接收数据块。
  15. @Override
  16. protected void process(List<Integer> chunks) {
  17. for (int number :chunks) {
  18. textArea.append(number + "\n");
  19. }
  20. }
  21. }
  22. JTextArea textArea = new JTextArea();
  23. final JProgressBar progressBar = new JProgressBar(0, 100);
  24. PrimeNumbersTask task = new PrimeNumbersTask(textArea, N);
  25. task.addPropertyChangeListener(new PropertyChangeListener() {
  26. public void propertyChange(PropertyChangeEvent evt) {
  27. if ("progress".equals(evt.getPropertyName())) {
  28. progressBar.setValue((Integer)evt.getNewValue());
  29. }
  30. }
  31. });
  32. task.execute();
  33. System.out.println(task.get()); //prints all prime numbers we have got

对于一般的耗时任务这样做是比较普遍的,但是有一些任务是一旦触发之后,会周期性的触发,如何做处理这种任务呢?JDK中提供了两个Timer类帮你完成定时任务,一个是javax.swing.Timer,一个java.util.Timer。使用它们的方法很简单,对于Swing的timer,使用方法如下:

  1. public void myActionPerformed(){
  2. //假设点击了某个按钮开始记时
  3. Action myAction=new AbstractAction(){
  4. public void actionPerformed(ActionEvent e){
  5. //做周期性的活动,比如显示当前时间
  6. Date date=new Date();
  7. jMyDate.setDate(date);//jMyDate是个假想的组件,能显示日期时间
  8. }
  9. };
  10. new Timer(1000, myAction).start();
  11. }

java.util.Timer类似,只不过使用TimerTask完成动作封装。注意这两个Timer有一个关键的区别:Swing的Timer的事件处理都是在事件调度线程上进行的,因而它里面的操作可以直接访问Swing组件。而java.util.Timer则可能在其他线程上,因而访问组件时要使用SwingUtilities.invokeLater和invokeAndWait来进行。这一点要记住。

下面表格列出Swing中组件及其模型的映射关系:

组件 Model接口 Model类型
JButton ButtonModel GUI状态
JToggleButton ButtonModel GUI状态/应用数据
JCheckBox ButtonModel GUI状态/应用数据
JRadioButton ButtonModel GUI状态/应用数据
JMenu ButtonModel GUI状态
JMenuItem ButtonModel GUI状态
JCheckBoxMenuItem ButtonModel GUI状态/应用数据
JRadioButtonMenuItem ButtonModel GUI状态/应用数据
JComboBox ComboBoxModel 应用数据
JProgressBar BoundedRangeModel GUI状态/应用数据
JScrollBar BoundedRangeModel GUI状态/应用数据
JSlider BoundedRangeModel GUI状态/应用数据
JTabbedPane SingleSelectionModel GUI状态
JList ListModel 应用数据
JList ListSelectionModel GUI状态
JTable TableModel 应用数据
JTable TableColumnModel GUI状态
JTree TreeModel 应用数据
JTree TreeSelectionModel GUI状态
JEditorPane Document 应用数据
JTextPane Document 应用数据
JTextArea Document 应用数据
JTextField Document 应用数据
JPasswordField Document 应用数据

上文中表格中,许多组件的数据抽象相似,只需一个接口而不用过分泛化时,组件可以共享同一模型定义。共享模型定义允许在不同组件之间自动连接。比如,JSlider和JScrollBar都使用BoundedRangeModel接口,因此可以在一个JScrollBar和一个JSlider之间共享同一个BoundedRangeModel实例,这样它们之间的状态就总是同步的。

状态化通知
支持状态化通知的模型根据它们的目的提供不同的Listener接口和事件对象。下表是这些模型接口和事件对象的类:

Model Listener Event
ListModel ListDataListener ListDataEvent
ListSelectionModel ListSelectionListener ListSelectionEvent
ComboBoxModel ListDataListener ListDataEvent
TreeModel TreeModelListener TreeModelEvent
TreeSelectionModel TreeSelectionListener TreeSelectionEvent
TableModel TableModelListener TableModelEvent
TableColumnModel TableColumnModelListener TableColumnModelEvent
Document DocumentListener DocumentEvent
Document UndoableEditListener UndoableEditEvent

使用SwingWorker

桌面应用程序员常见的错误是误用Swing事件调度线程(Event Dispatch Thread, EDT)。他们要么从非UI线程访问UI组件,要么不考虑事件执行顺序,要么不使用独立任务线程而在EDT线程上执行耗时任务,结果使编写的应用程序变得响应迟钝、速度很慢。耗时计算和输入/输出(IO)密集型任务不应放在Swing EDT上运行。


Swing多线程基础

一个Swing程序中一般有下面三种类型的线程:

  • 初始化线程(Initial Thread)
  • UI事件调度线程(EDT)
  • 任务线程(Worker Thread)

每个程序必须有一个main方法,这是程序的入口。该方法运行在初始化或启动线程上。初始化线程读取程序参数并初始化一些对象。在许多Swing程序中,该线程主要目的是启动程序的图形用户界面(GUI)。一旦GUI启动后,对于大多数事件驱动的桌面程序来说,初始化线程的工作就结束了。

Swing程序只有一个EDT,该线程负责GUI组件的绘制和更新,通过调用程序的事件处理器来响应用户交互。所有事件处理都是在EDT上进行的,程序同UI组件和其基本数据模型的交互只允许在EDT上进行,所有运行在EDT上的任务应该尽快完成,以便UI能及时响应用户输入。

Swing编程时应该注意以下两点:

  • 1.从其他线程访问UI组件及其事件处理器会导致界面更新和绘制错误。
  • 2.在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理。
  • 3.应使用独立的任务线程来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大树据量的文件。

总之,任何干扰或延迟UI事件的处理只应该出现在独立任务线程中;在初始化线程或任务线程同Swing组件或其缺省数据模型进行的交互都是非线程安全性操作。

SwingWorker类帮你管理任务线程和Swing EDT之间的交互,尽管SwingWorker不能解决并发线程中遇到的所有问题,但的确有助于分离Swing EDT和任务线程,使它们各负其责:对于EDT来说,就是绘制和更新界面,并响应用户输入;对于任务线程来说,就是执行和界面无直接关系的耗时任务和I/O密集型操作。


使用合适线程

初始化线程运行程序的main方法,该方法能处理许多任务。但在典型的Swing程序中,其主要任务就是创建和运行应用程序的界面。创建UI的点,也就是程序开始将控制权转交给UI时的点,往往是同EDT交互出现问题的第一个地方。

许多程序使用下面方法启动界面,但这是错误的启动UI界面的方法:

  1. public class MainFrame extends javax.swing.JFrame {
  2. ...
  3. public static void main(String[] args) {
  4. new MainFrame().setVisible(true);
  5. }
  6. }

尽管这种错误出现在开始,但仍然违反了不应在EDT外的其他线程同Swing组件交互的原则。这个错误尤其容易犯,线程同步问题虽然不是马上显示出来,但是还要注意避免这样书写。

正确启动UI界面应该如下:

  1. public class MainFrame extends javax.swing.JFrame {
  2. ...
  3. public static void main(String[] args) {
  4. SwingUtilities.invokeLater(new Runnable() {
  5. public void run() {
  6. new MainFrame().setVisible(true);
  7. }
  8. });
  9. }
  10. }

在初始化线程中使用invokeLater方法能正确的初始化程序界面。就像前面文章所提到的,此方法是异步执行的,也就是说调用会立即返回。创建界面后,大部分初始化线程基本上就结束了。

通常有两种办法调用此方法:

  • SwingUtilities.invokeLater
  • EventQueue.invokeLater

两个方法都是正确的,选择任何一个都可以。实际上,SwingUtilities版只是一个薄薄的封装方法,它直接转而调用EventQueue.invokeLater。因为Swing框架本身经常调用SwingUtilities,使用SwingUtilities可以减少程序引入的类。

另种将任务放到EDT执行的方法是SwingUtilities.invokeAndWait,不像invokeLater,invokeAndWait方法是阻塞执行的,它在EDT上执行Runnnable任务,直到任务执行完了,该方法才返回调用线程。

invokeLater和invokeAndWait都在事件派发队列中的所有事件都处理完之后才执行它们的Runnable任务,也就是说,这两个方法将Runnable任务放在事件队列的末尾。

注意:虽然可以在其他线程上调用invokeLater,也可以在EDT上调用invokeLater,但是千万不要在EDT线程上调用invokeAndWait方法!很容易理解,这样做会造成线程竞争,程序就会陷入死锁。


将EDT线程仅用于GUI任务

Swing框架负责管理组件绘制、更新以及EDT上的线程处理。可以想象,该线程的事件队列很繁忙,几乎每一次GUI交互和事件都是通过它完成。事件队列的上任务必须非常快,否则就会阻塞其他任务的执行,使队列里阻塞了很多等待执行的事件,造成界面响应不灵活,让用户感觉到界面响应速度很慢,使他们失去兴趣。理想情况下,任何需时超过30到100毫秒的任务不应放在EDT上执行,否则用户就会觉察到输入和界面响应之间的延迟。


SwingWorker基础

SwingWorker的定义如下:
public abstract class SwingWorker<T,V> extends Object implements RunnableFuture

SwingWorker是抽象类,因此必须继承它才能执行所需的特定任务。注意该类有两个类型参数:T及V。T是doInBackground和get方法的返回类型,V是publish和process方法要处理的数据类型。

该类实现了java.util.concurrent.RunnableFuture接口。RunnableFuture接口是Runnable和Future两个接口的简单封装。由于SwingWorker实现了Runnable接口,因此SwingWorker有一个run方法。由于SwingWorker实现了Future接口,因此SwingWorker产生类型为T的结果值并提供同线程交互的方法。

SwingWorker实现了所有的接口方法,实际上你仅需要实现以下SwingWorker的抽象方法:
protected T doInBackground() throws Exception
doInBackground方法作为任务线程的一部分执行,它负责完成线程的基本任务,并以返回值来作为线程的执行结果。继承类须覆盖该方法并确保包含或代理任务线程的基本任务。不要直接调用该方法,应使用任务对象的execute方法来调度执行。

在获得执行结果后应使用SwingWorker的get方法获取doInBackground方法的结果。可以在EDT上调用get方法,但该方法将一直处于阻塞状态,直到任务线程完成。最好只有在知道结果时才调用get方法,这样用户便不用等待。为防止阻塞,可以使用isDone方法来检验doInBackground是否完成。另外调用方法get(long timeout, TimeUnit unit)将会一直阻塞直到任务线程结束或超时。

获取任务结果的最好地方是在done方法内:
protected void done()

在doInBackground方法完成之后,SwingWorker调用done方法。如果任务需要在完成后使用线程结果更新GUI组件或者做些清理工作,可覆盖done方法来完成它们。这儿是调用get方法的最好地方,因为此时已知道线程任务完成了,SwingWorker在EDT上激活done方法,因此可以在此方法内安全地和任何GUI组件交互。

没必要等到线程完成就可以获得中间结果。中间结果是任务线程在产生最后结果之前就能产生的数据。当任务线程执行时,它可以发布类型为V的中间结果,覆盖process方法来处理中间结果。后文还将提供这些方法的更多详细信息。当属性改变时,SwingWorker实例能通知处理器,SwingWorker有两个重要的属性:状态和进程。任务线程有几种状态,以下面SwingWorker.StateValue枚举值来表示:

  • PENDING
  • STARTED
  • DONE

任务线程一创建就处于PENDING状态,当doInBackground方法开始时,任务线程就进入STARTED状态,当doInBackground方法完成后,任务线程就处于DONE状态,随着线程进入各个阶段,SwingWorker超类自动设置这些状态值。你可以添加处理器,当这些属性发生变化来接收通知。

最后,任务对象有一个进度属性,随着任务进展时,可以将这个属性从0更新到100标识任务进度,当该属性发生变化时,任务通知处理器进行处理。


Swing的第一推动力

Swing通过AWT的事件循环系统来推动整个系统的运行,这个AWT的事件系统就是Swing系统的这个“第一推动力”。

不像一般GUI系统事件循环是由单个线程完成的,Swing事件循环实际上存在两个线程。具体到Windows上来说,一个线程叫AWT-Windows,它负责从操作系统获取底层事件,并将事件处理后翻译成Swing能懂的事件,并放入到Swing的系统事件队列(EventQueue)中;另一个线程叫EventQueue-0,该线程就是所谓的EDT(Event Dispatch Thread),它负责从事件队列中获取事件,并分派到Swing组件中,最终产生有意义的动作事件传递给组件事件处理器。有时EDT还负责将事件队列中的事件进行预处理,比如多个连续的Paint事件合并成一个等等。

和一般GUI工具系统不同,Swing的这种双事件处理线程有其设计目的。总的来说这种事件处理线程模型增加了Swing图形系统的灵活性和可扩展性,为Swing实现高级的功能预留下了扩展空间。

Swing著名的"灰框(gray rect)"问题就是利用这种巧妙结构实现的。单单就EDT的模型来说,Swing存在普通GUI系统常见的线程占用问题。所谓的"灰框"问题是指,某些Swing程序由于编写较差,将长时间任务放在EDT上进行。如果此时恰巧有一个窗口遮住了Swing程序,当用户移开覆盖窗口时,由于任务阻塞了EDT,使Paint事件得不到及时处理,造成Swing界面出现灰色方框的现象。

JDK 1.6采用了如下方法解决了这个问题:当被遮挡的窗口被暴露时,AWT-Windows线程获得到了这个EXPOSE事件,在将事件翻译成Paint事件给事件队列之前,AWT-Windows先从操作系统的图形缓冲里获取窗口被遮挡前的图像(注意某些Linux图形系统并不支持这种缓冲,所以Swing在某些Linux系统上并没有解决这个问题),然后将这个图像采用bltbit的方法画在暴露出的灰框上,然后才把Paint事件发送给Swing事件队列。这儿的关键就在于获取底层事件的线程AWT-Windows同Swing的EDT不是一个线程,因此EDT虽然被长时间任务阻塞了,但是AWT-Windows这个线程并没有被阻塞。因此可以及时的处理窗口暴露事件,避免了灰框问题。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注