Swing API的设计目标是强大灵活和易用特别地我们希望能让程序员们方便地建立新的Swing组件不论是从头开始还是通过扩展我们所提供的一些组件
出于这个目的我们不要求Swing组件支持多线程访问相反我们向组件发送请求并在单一线程中执行请求
本文讨论线程和Swing组件目的不仅是为了帮助你以线程安全的方式使用Swing API而且解释了我们为什么会选择现在这样的线程方案
本文包括以下内容
单线程规则Swing线程在同一时刻仅能被一个线程所访问一般来说这个线程是事件派发线程(eventdispatching thread)
规则的例外有些操作保证是线程安全的
事件分发如果你需要从事件处理(eventhandling)或绘制代码以外的地方访问UI那么你可以使用SwingUtilities类的invokeLater()或invokeAndWait()方法
创建线程如果你需要创建一个线程——比如用来处理一些耗费大量计算能力或受I/O能力限制的工作——你可以使用一个线程工具类如SwingWorker或Timer
为什么我们这样实现Swing我们将用一些关于Swing的线程安全的背景资料来结束这篇文章
Swing的规则是
一旦Swing组件被具现化(realized)所有可能影响或依赖于组件状态的代码都应该在事件派发线程中执行
这个规则可能听起来有点吓人但对许多简单的程序来说你用不着为线程问题操心在我们深入如何撰写Swing代码之前让我们先来定义两个术语具现化(realized)和事件派发线程(eventdispatching thread)
具现化的意思是组建的paint()方法已经或可能会被调用一个作为顶级窗口的Swing组件当调用以下方法时将被具现化setVisible(true)show()或(可能令你惊奇)pack()当一个窗口被具现化它包含的所有组件都被具现化另一个具现化一个组件的方法是将它放入到一个已经具现化的容器中稍后你会看到一些对组件具现化的例子
事件派发线程是执行绘制和事件处理的线程例如paint()和actionPerformed()方法会自动在事件派发线程中执行另一个将代码放到事件派发线程中执行的方法是使用SwingUtilities类的invokeLater()方法
所有可能影响一个已具现化的Swing组件的代码都必须在事件派发线程中执行但这个规则有一些例外
有些方法是线程安全的在Swing API的文档中线程安全的方法用以下文字标记
This method is thread safe although most Swing methods are not
(这个方法是线程安全的尽管大多数Swing方法都不是)
一个应用程序的GUI常常可以在主线程中构建和显示下面的典型代码是安全的只要没有(Swing或其他)组件被具现化
public class MyApplication
{
public static void main(String[] args)
{
JFrame f = new JFrame(Labels); // 在这里将各组件
// 加入到主框架……
fpack();
fshow();
// 不要再做任何GUI工作……
}
}
上面所示的代码全部在main线程中运行对fpack()的调用使得JFrame以下的组件都被具现化这意味着fshow()调用是不安全的且应该在事件派发线程中执行尽管如此只要程序还没有一个看得到的GUIJFrame或它的里面的组件就几乎不可能在fshow()返回前收到一个paint()调用因为在fshow()调用之后不再有任何GUI代码于是所有GUI工作都从主线程转到了事件派发线程因此前面所讨论的代码实际上是线程安全的
一个applet的GUI可以在init()方法中构造和显示现有的浏览器都不会在一个applet的init()和start()方法被调用前绘制它因而在一个applet的init()方法中构造GUI是安全的只要你不对applet中的对象调用show()或setVisible(true)方法
要顺便一提的是如果applet中使用了Swing组件就必须实现为JApplet的子类并且组件应该添加到的JApplet内容窗格(content pane)中而不要直接添加到JApplet对任何applet你都不应该在init()或start()方法中执行费时的初始化操作而应该启动一个线程来执行费时的任务
下述JComponent方法是安全的可以从任何线程调用repaint()revalidate()和invalidate()repaint()和revalidate()方法为事件派发线程对请求排队并分别调用paint()和validate()方法invalidate()方法只在需要确认时标记一个组件和它的所有直接祖先
监听者列表可以由任何线程修改调用addListenerTypeListener()和removeListenerTypeListener()方法总是安全的对监听者列表的添加/删除操作不会对进行中的事件派发有任何影响
注意revalidate()和旧的validate()方法之间的重要区别是revalidate()会缓存请求并组合成一次validate()调用这和repaint()缓存并组合绘制请求类似
大多数初始化后的GUI工作自然地发生在事件派发线程一旦GUI成为可见大多数程序都是由事件驱动的如按钮动作或鼠标点击这些总是在事件派发线程中处理的
不过总有些程序需要在GUI成为可见后执行一些非事件驱动的GUI工作比如
在成为可用前需要进行长时间初始化操作的程序这类程序通常应该在初始化期间就显示出GUI然后更新或改变GUI初始化过程不应该在事件派发线程中进行否则重绘组件和事件派发会停止尽管如此在初始化之后GUI的更新/改变还是应该在事件派发线程中进行理由是线程安全
必须响应非AWT事件来更新GUI的程序例如想象一个服务器程序从可能运行在其他机器上的程序得到请求这些请求可能在任何时刻到达并且会引起在一些可能未知的线程中对服务器的方法调用这个方法调用怎样更新GUI呢?在事件派发线程中执行GUI更新代码
SwingUtilities类提供了两个方法来帮助你在事件派发线程中执行代码
invokeLater()要求在事件派发线程中执行某些代码这个方法会立即返回不会等待代码执行完毕
invokeAndWait()行为与invokeLater()类似除了这个方法会等待代码执行完毕一般地你可以用invokeLater()来代替这个方法
下面是一些使用这几个API的例子请同时参阅《The Java Tutorial》中的BINGO example尤其是以下几个类CardWindowControlPanePlayer和OverallStatusPane
使用invokeLater()方法
你可以从任何线程调用invokeLater()方法以请求事件派发线程运行特定代码你必须把要运行的代码放到一个Runnable对象的run()方法中并将此Runnable对象设为invokeLater()的参数invokeLater()方法会立即返回不等待事件派发线程执行指定代码这是一个使用invokeLater()方法的例子
Runnable doWorkRunnable = new Runnable()
{
public void run()
{
doWork();
}
};
SwingUtilitiesinvokeLater(doWorkRunnable);
使用invokeAndWait()方法
invokeAndWait()方法和invokeLater()方法很相似除了invokeAndWait()方法会等事件派发线程执行了指定代码才返回在可能的情况下你应该尽量用invokeLater()来代替invokeAndWait()如果你真的要使用invokeAndWait()请确保调用invokeAndWait()的线程不会在调用期间持有任何其他线程可能需要的锁
这是一个使用invokeAndWait()的例子
void showHelloThereDialog() throws Exception
{
Runnable showModalDialog = new Runnable()
{
public void run()
{
JOptionPaneshowMessageDialog( myMainFrame Hello There);
}
};
SwingUtilitiesinvokeAndWait (showModalDialog);
}
类似地假设一个线程需要对GUI的状态进行存取比如文本域的内容它的代码可能类似这样
void printTextField()
throws Exception {
final String[] myStrings = new String[];
Runnable getTextFieldText = new Runnable() {
public void run() {
myStrings[] = textFieldgetText();
myStrings[] = textFieldgetText();
}
};
SwingUtilitiesinvokeAndWait (getTextFieldText);
Systemoutprintln(myStrings[] + + myStrings[]);}
如果你能避免使用线程最好这样做线程可能难于使用并使得程序的debug更困难一般来说对于严格意义下的GUI工作线程是不必要的比如对组件属性的更新
不管怎么说有时候线程是必要的下列情况是使用线程的一些典型情况
执行一项费时的任务而不必将事件派发线程锁定例子包括执行大量计算的情况会导致大量类被装载的情况(如初始化)和为网络或磁盘I/O而阻塞的情况
重复地执行一项操作通常在两次操作间间隔一个预定的时间周期
要等待来自客户的消息
你可以使