5.4.2 事件分发线程
AWT和Swing中对用户界面组件的事件处理都是单线程的。对于每个事件队列,都有一个专门的线程负责进行事件的分发和处理。在分发事件的时候,事件对应的处理方法也是在这个线程中被调用的。这就要求在一个事件的处理方法中包含的逻辑应该尽可能地少,可以在较短的时间内完成,否则可能造成队列中的其他事件长时间处于等待状态,给用户的感觉就是程序的用户界面在较长的时间内失去了响应。比如,当用户界面的窗口从最小化状态恢复的时候,需要重新绘制界面,系统会在事件队列中添加一个新的重新绘制的事件来指示各组件重新绘制自身的界面。如果当前事件队列中已经有一个事件正在处理,而这个事件的处理又很耗时,那么这个新的重新绘制的事件在较长的时间内不会被分发,会导致用户界面成为一片空白,没有任何内容。
从线程的安全性方面来说,AWT和Swing的大部分API都不是线程安全的,这包括绝大部分操作用户界面的API。如果多个线程同时对界面进行更新,可能会造成界面的显示问题。实际上,要实现一个线程安全的用户界面组件库是一件很困难的事情。AWT和Swing没有选择花费大量的精力去实现线程的安全性,而是采取了一种简单的做法,即要求所有与界面相关的操作都在单一的线程中进行。这种单线程的界面更新方式自然可以避免线程安全性的问题,这个线程就是前面提到的事件队列对应的事件分发线程(Event Dispatch Thread, EDT)。
在大多数情况下,事件分发线程都在后台工作,开发人员也不需要了解它的存在。与事件分发线程产生联系的一个典型场景是需要执行长时间任务的时候。如果一个任务的执行时间很长,那么由事件分发线程来处理就不合适,因为这样会导致其无法及时处理其他事件。合理的做法是由另外一个工作线程来处理。不过要注意的是,如果在处理中需要更新用户界面,应该交由事件分发线程来处理,而不是由当前工作线程处理。这么做的目的是为了保证与界面相关的操作都在同一个事件分发线程中执行,避免线程安全性的问题。
AWT和Swing都提供了相关的方法来在事件分发线程中执行更新界面的操作。代码清单5-9给出了一个AWT中事件分发线程的使用示例。示例中使用一个后台线程来计算π的值,计算出结果后显示在一个标签组件中。因为需要在事件分发线程中执行界面相关的操作,对java.awt.Label类的setText方法的调用需要封装在EventQueue类的invokeLater方法中。方法invokeLater参数中的java.lang.Runnable接口的实现对象会在系统默认事件队列对应的分发线程中执行。与invokeLater方法作用相似的方法是invokeAndWait,两者的区别在于:invokeLater方法只是把Runnable接口实现对象放入事件队列中就立刻返回,实际的执行需要等待队列中的其他事件处理完毕,所以是一个异步操作;而invokeAndWait方法会等待队列中的所有其他事件及Runnable接口实现对象的run方法执行完成之后才返回,所以是一个同步操作。开发人员可以根据需要选择异步方式还是同步方式。
代码清单5-9 AWT中事件分发线程的使用示例
public class CalculatePi{
public void calculate(){
Frame frame=new Frame();
Label label=new Label();
frame.add(label);
frame.setSize(400,300);
frame.setVisible(true);
new CalculateThread(label).start();
}
private static class CalculateThread extends Thread{
double sum=0.0,term, sign=1.0;
int N=3010001000;
private Label label;
public CalculateThread(Label label){
this.label=label;
}
public void run(){
for(int k=0;k<N;k++){
term=1.0/(2.0*k+1.0);
sum=sum+sign*term;
if(k%(N/100)==0){
EventQueue.invokeLater(new Runnable(){
public void run(){
label.setText(Double.toString(4*sum));
}
});
}
sign=-sign;
}
}
}
}
Swing中也有与AWT中的EventQueue类的invokeLater和invokeAndWait方法功能相同的方法,这些方法在javax.swing.SwingUtilities类中。SwingUtilities类中对应的方法的名称也是invokeLater和invokeAndWait,调用时的行为也是相同的。对于一个使用Swing的桌面应用来说,应该把创建用户界面相关的逻辑都封装在SwingUtilities的invokeLater方法中。如代码清单5-10所示,程序的main方法应该对创建用户界面的createUI的方法的调用进行封装,以确保该方法在事件分发线程中执行。
代码清单5-10 SwingUtilities类的invokeLater方法的使用示例
public static void main(String[]args){
SwingUtilities.invokeLater(new Runnable(){
public void run(){
createUI();
}
});
}