本文关于Swing中的多线程,发表于1998年4月。一个月后,我们发表了另一篇文章《使用Swing Worker线程
》,该文更深入地讨论了这一主题。要更好地了解多线程在Swing中如何工作,我们建议你把这两篇文章都看一下。
注意
:在2000年9月我们修改了这篇文章和它的例子以适用于一个更新版本的SwingWorker类。SwingWorker类的这个版本修正了一些微妙的线程bug。
Swing API的设计目标是强大、灵活和易用。特别地,我们希望能让程序员们方便地建立新的Swing组件,不论是从头开始还是通过扩展我们所提供的一些组件。
出于这个目的,我们不要求Swing组件支持多线程访问。相反,我们向组件发送请求并在单一线程中执行请求。
本文讨论线程和Swing组件。目的不仅是为了帮助你以线程安全的方式使用Swing API,而且解释了我们为什么会选择现在这样的线程方案。
本文包括以下内容:
-
单线程规则
:Swing线程在同一时刻仅能被一个线程所访问。一般来说,这个线程是事件派发线程(event-dispatching thread)。
-
规则的例外
:有些操作保证是线程安全的。
-
事件分发
:如果你需要从事件处理(event-handling)或绘制代码以外的地方访问UI,那么你可以使用SwingUtilities类的invokeLater()或invokeAndWait()方法。
-
创建线程
:如果你需要创建一个线程――比如用来处理一些耗费大量计算能力或受I/O能力限制的工作――你可以使用一个线程工具类如SwingWorker或Timer。
-
为什么我们这样实现Swing
:我们用一些关于Swing的线程安全的背景资料来结束这篇文章。
Swing的规则是:
一旦Swing组件被具现化(realized),所有可能影响或依赖于组件状态的代码都应该在事件派发线程中执行。
这个规则可能听起来有点吓人,但对许多简单的程序来说,你用不着为线程问题操心。在我们深入如何撰写Swing代码之前,让我们先来定义两个术语:具现化(realized)和事件派发线程(event-dispatching 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"
);
- // 在这里将各组件
- // 加入到主框架……
- f.pack();
- f.show();
- // 不要再做任何GUI工作……
- }
- }
上面所示的代码全部在“main”线程中运行。对f.pack()的调用使得JFrame以下的组件都被具现化。这意味着,f.show()调用是不安全的且应该在事件派发线程中执行。尽管如此,只要程序还没有一个看得到的GUI,JFrame或它的里面的组件就几乎不可能在f.show()返回前收到一个paint()调用。因为在f.show()调用之后不再有任何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
”,尤其是以下几个类:CardWindow
、ControlPane
、Player
和OverallStatusPane
。
使用invokeLater()方法
你可以从任何线程调用invokeLater()方法以请求事件派发线程运行特定代码。你必须把要运行的代码放到一个Runnable对象的run()方法中,并将此Runnable对象设为invokeLater()的参数。invokeLater()方法会立即返回,不等待事件派发线程执行指定代码。这是一个使用invokeLater()方法的例子:
-
Runnable
doWorkRunnable = new
Runnable
() {
- public
void
run() { doWork(); }
- };
-
SwingUtilities
.invokeLater(doWorkRunnable);
使用invokeAndWait()方法
invokeAndWait()方法和invokeLater()方法很相似,除了invokeAndWait()方法会等事件派发线程执行了指定代码才返回。在可能的情况下,你应该尽量用invokeLater()来代替invokeAndWait()。如果你真的要使用invokeAndWait(),请确保调用invokeAndWait()的线程不会在调用期间持有任何其他线程可能需要的锁。
这是一个使用invokeAndWait()的例子:
-
void
showHelloThereDialog()
- throws
Exception
{
- Runnable
showModalDialog = new
- Runnable
() {
- public
void
run() {
- JOptionPane
.showMessageDialog(
- myMainFrame, "Hello There"
);
- }
- };
- SwingUtilities
.invokeAndWait
- (showModalDialog);
- }
类似地,假设一个线程需要对GUI的状态进行存取,比如文本域的内容,它的代码可能类似这样:
-
void
printTextField() throws
Exception
{
- final
String
[] myStrings =
- new
String
[2];
- Runnable
getTextFieldText =
- new
Runnable
() {
- public
void
run() {
- myStrings[0] =
- textField0.getText();
- myStrings[1] =
- textField1.getText();
- }
- };
- SwingUtilities
.invokeAndWait
- (getTextFieldText);
- System
.out.println(myStrings[0]
- + " "
+ myStrings[1]);
- }
如果你能避免使用线程,最好这样做。线程可能难于使用,并使得程序的debug更困难。一般来说,对于严格意义下的GUI工作,线程是不必要的,比如对组件属性的更新。
不管怎么说,有时候线程是必要的。下列情况是使用线程的一些典型情况:
- 执行一项费时的任务而不必将事件派发线程锁定。例子包括执行大量计算的情况,会导致大量类被装载的情况(如初始化),和为网络或磁盘I/O而阻塞的情况。
- 重复地执行一项操作,通常在两次操作间间隔一个预定的时间周期。
- 要等待来自客户的消息。
你可以使用两个类来帮助你实现线程:
- SwingWorker:创建一个后台线程来执行费时的操作。
- Timer:创建一个线程来执行或多次执行某些代码,在两次执行间间隔用户定义的延迟。
使用SwingWorker类
SwingWorker类在SwingWorker.java
中实现,这个类并不包含在Java的任何发行版中,所以你必须单独下载它。
SwingWorker类做了所有实现一个后台线程所需的肮脏工作。虽然许多程序都不需要后台线程,后台线程在执行费时的操作时仍然是很有用的,它能提高程序的性能观感。
SwingWorker's get() method. Here's an example of using SwingWorker:
要使用SwingWorker类,你首先要实现它的一个子类。在子类中,你必须实现construct()方法还包含你的长时间操作。当你实例化SwingWorker的子类时,SwingWorker创建一个线程但并不启动它。你要调用你的SwingWorker对象的start()方法来启动线程,然后start()方法会调用你的construct()方法。当你需要construct()方法返回的对象时,可以调用SwingWorker类的get()方法。这是一个使用SwingWorker类的例子:
- ...// 在main方法中:
- final
SwingWorker worker =
- new
SwingWorker() {
- public
Object
construct() {
- return
new
- expensiveDialogComponent();
- }
- };
- worker.start();
- ...// 在动作事件处理方法中:
- JOptionPane
.showMessageDialog
- (f, worker.get());
当程序的main()方法调用start()方法,SwingWorker启动一个新的线程来实例化ExpensiveDialogComponent。main()方法还构造了由一个窗口和一个按钮组成的GUI。
当用户点击按钮,程序将阻塞,如果必要,阻塞到ExpensiveDialogComponent创建完成。然后程序显示一个包含ExpensiveDialogComponent的模式对话框。你可以在MyApplication.java
找到整个程序。
使用Timer类
Timer类通过一个ActionListener来执行或多次执行一项操作。你创建定时器的时候可以指定操作执行的频率,并且你可以指定定时器的动作事件的监听者(action listener)。启动定时器后,动作监听者的actionPerformed()方法会被(多次)调用来执行操作。
定时器动作监听者(action listener)定义的actionPerformed()方法将在事件派发线程中调用。这意味着你不必在其中使用invokeLater()方法。
这是一个使用Timer类来实现动画循环的例子:
-
public
class
AnimatorApplicationTimer
- extends
JFrame
implements
- ActionListener
{
- ...//在这里定义实例变量
- Timer
timer;
- public
AnimatorApplicationTimer(...) {
- ...
- // 创建一个定时器来
- // 来调用此对象action handler。
- timer = new
Timer
(delay, this
);
- timer.setInitialDelay(0);
- timer.setCoalesce(true
);
- ...
- }
- public
void
startAnimation() {
- if
(frozen) {
- // 什么都不做。应用户要求
- // 停止变换图像。
- } else
{
- // 启动(或重启动)动画!
- timer.start();
- }
- }
- public
void
stopAnimation() {
- // 停止动画线程。
- timer.stop();
- }
- public
void
actionPerformed
- (ActionEvent
e) {
- // 进到下一帧动画。
- frameNumber++;
- // 显示。
- repaint();
- }
- ...
- }
在一个线程中执行所有的用户界面代码有这样一些优点:
-
组件开发者不必对线程编程有深入的理解
:像ViewPoint和Trestle这类工具包中的所有组件都必须完全支持多线程访问,使得扩展非常困难,尤其对不精通线程编程的开发者来说。最近的一些工具包如SubArctic和IFC,都采用和Swing类似的设计。
-
事件以可预知的次序派发
:invokeLater()排队的runnable对象从鼠标和键盘事件、定时器事件、绘制请求的同一个队列派发。在一些组件完全支持多线程访问的工具包中,组件的改变被变化无常的线程调度程序穿插到事件处理过程中。这使得全面测试变得困难甚至不可能。
-
更低的代价
:尝试小心锁住临界区的工具包要花费实足的时间和空间在锁的管理上。每当工具包中调用某个可能在客户代码中实现的方法时(如public类中的任何public和protected方法),工具包都要保存它的状态并释放所有锁,以便客户代码能在必要时获得锁。当控制权交回到工具包,工具包又必须重新抓住它的锁并恢复状态。所有应用程序都不得不负担这一代价,即使大多数应用程序并不需要对GUI的并发访问。
这是的SubArctic Java Toolkit的作者对在工具包中支持多线程访问的问题的描述:
我们的基本信条是,当设计和建造多线程应用程序,尤其是那些包括GUI组件的应用程序时,必须保证极端小心。线程的使用可能会很有欺骗性。在许多情况下,它们表现得能够极好的简化编成,使得设计“专注于单一任务的简单自治实体”成为可能。在一些情况下它们的确简化了设计和编码。然而,在几乎所有的情况下,它们都使得调试、测试和维护的困难大大增加甚至成为不可能。无论大多数程序员所受的训练、他们的经验和实践,还是我们用来帮助自己的工具,都不是能够用来对付非决定论的。例如,全面测试(这总是困难的)在bug依赖于时间时是几乎不可能的。尤其对于Java来说,一个程序要运行在许多不同类型的机器的操作系统平台上,并且每个程序都必须在抢先和非抢先式调度下都能正常工作。
由于这些固有的困难,我们力劝你三思是否绝对有使用线程的必要。尽管如此,有些情况下使用线程是必要的(或者是被其他软件包强加的),所以subArctic提供了一个线程安全的访问机制。本章讨论了这一机制和怎样在一个独立线程中安全地操作交互树。
他们所说的线程安全机制非常类似于SwingUtilities类提供的invokeLater()和invokeAndWait()方法。
分享到:
相关推荐
标题中的“JAVA+多线程+swing和awt技术+ 飞机大战+学习Java者”揭示了这个压缩包包含的内容是关于Java编程的,特别是涉及到多线程和图形用户界面(GUI)开发,使用了Java的Swing和AWT库来实现一个“飞机大战”的游戏...
简单的摇骰子游戏、使用线程和swing、awt.zip java作业。简单的摇骰子游戏、使用线程和swing、awt.zip java作业。简单的摇骰子游戏、使用线程和swing、awt.zip java作业。简单的摇骰子游戏、使用线程和swing、awt....
任务线程与Swing组件的交互必须在EDT中进行,通常通过`SwingUtilities.invokeLater`或`invokeAndWait`方法来实现。 Swing编程中应注意以下几点: - **禁止直接从其他线程访问UI组件**:这样做可能导致界面更新和...
### Java开发中的线程安全选择与Swing 在Java开发中,Swing作为构建桌面应用程序图形用户界面(GUI)的主要工具之一,其设计目标是为了提供一个强大、灵活且易于使用的框架。Swing允许开发者轻松创建自定义组件或...
【基于Swing的多线程聊天室】是一个Java应用程序,它利用了Swing库来构建图形用户界面(GUI)并采用多线程技术实现多用户之间的实时通信。Swing是Java Standard Edition(Java SE)的一部分,提供了丰富的组件库用于...
Java线程与Swing - **多线程基础**:Swing应用程序通常运行在一个事件分发线程(Event Dispatch Thread, EDT)中,因此了解Java线程的基本概念对于编写高效、响应良好的Swing程序至关重要。 - **线程安全**:由于...
【标题】"java多线程+Socket+Swing做的局域网聊天程序"涉及的核心知识点主要涵盖Java编程、多线程、网络通信以及图形用户界面设计。以下将详细阐述这些关键概念及其在实现局域网聊天程序中的应用。 **1. Java编程**...
在Java开发中,Swing库是用来构建图形用户界面(GUI)的工具包,而多线程则是提升程序并发性能和响应能力的关键技术。Swing虽然是Java语言的一部分,但它设计为单线程模型,主要是为了简化GUI编程并避免复杂的同步...
Swing线程的深入理解和SwingWorker基础知识介绍 Swing线程是Java程序设计中的一种重要概念,用于处理图形用户界面(GUI)中的线程问题。在本文中,我们将深入探讨Swing线程的理解和SwingWorker基础知识,并通过实例...
Java Swing多线程下载器是一种利用Java Swing库构建的图形用户界面(GUI)应用程序,它具备多线程下载功能,并支持断点续传。这样的工具类似于我们熟知的迅雷下载管理器,允许用户同时下载多个文件,提高下载速度,...
在本项目中,“swing界面socket多线程聊天室”是一个基于Java Swing的客户端-服务器通信应用,它利用TCP协议来实现实时的聊天功能。这个系统不仅提供了群聊和私聊的功能,还允许用户发送文件,并且具备用户登录与...
在“JAVASWING多线程产生随机球”的项目中,开发者利用Swing创建了一个互动的应用程序,用户可以通过鼠标点击在界面上生成一个球体,这个球体会以随机的方向和速度在窗口内移动。下面将详细解释这个项目涉及的知识点...
总的来说,Java Swing聊天室项目结合了GUI设计、事件处理、网络编程和多线程技术,是一个全面展示Java桌面应用开发能力的好例子。通过学习和分析这样的项目,开发者不仅可以提升Swing应用的构建技巧,还能加深对Java...
在Java Swing中创建动态、交互式的应用,线程管理是一个关键的概念。当我们谈论"下雪效果"时,这意味着在Swing程序中实现一个模拟下雪场景的动画。 首先,要实现下雪效果,我们需要理解Swing的事件处理机制。Swing...
第一篇文章《线程与Swing》,解释了Swing的单线程规则。这篇文章现在可以在TheSwingConnectionArchive找到。第二篇文章《使用SwingWorker线程》,演示了如何使用SwingWorker线程工具类。它也可以在存档中找到。本文...
线程内部可能包含电梯的状态管理(如上行、下行、停止)、目的地处理以及与其它线程的同步机制(如wait()和notify()方法)。 接着是"ListFrame.java"。这个名字暗示着它可能是一个Swing的JFrame子类,用于构建用户...
Java Swing 是一个用于构建桌面应用程序的用户界面工具包,它是Java Foundation Classes (JFC) 的一部分...对于初学者来说,通过研究提供的 "几个swing多线程的例子",可以更好地理解如何在实践中结合 Swing 和多线程。
Swing 线程之 SwingUtilities invokeLater Swing 线程模型是 Java 中一个非常重要的概念,它是 Java 的图形用户界面(GUI)程序设计中的一部分。Swing 是基于 MVC 模式(Model-View-Controller)设计的,它提供了一...