Swing与线程

来源:互联网 发布:java工程师日工作时间 编辑:程序博客网 时间:2024/05/07 18:24

swing与线程

使用线程是为了提高程序的响应速度,当程序需要做某些很耗时的任务时,不应阻塞用户接口而应启动另一个工作器线程。但是我们必须小心工作器线程所做的事情。
Swing不是线程安全的,不要尝试在多个线程中操作用户界面元素,否则程序可能崩溃。
Swing为什么不设计成线程安全的:首先同步要耗费时间(Swing的速度本来就令人不满了);使用线程安全包的用户界面程序员不能很好的使其同步,容易产生死锁的构件。

每一个Java应用程序都开始于一个主线程中的main方法。在Swing程序中main方法处理:
1.首先调用构造器在框架窗口中排列构件;
2.然后调用框架窗口的setVisible方法。

当显式第一个窗口时,第二个线程(事件分发线程)被创建。所有事件的通知:如调用actionPerformed方法或paintComponent方法,都在事件派发线程中执行。而主线程会保持运行直到main方法运行结束(一般来说main方法在窗口显式不久就退出了)。

其他线程像:向事件队列发布事件的线程,都在后台运行,只是这些线程对应用程序员都是不可见的。所有代码运行在事件派发线程中。
当你将线程和Swing一起使用时应遵守下列规则:
1.如果一个动作占用的时间很长,就启动一个新的线程来执行他。因为如果事件派发线程执行的任务占用了大量的时间,那么用户界面几乎不能及时响应任何事件了。
2.如果一个动作在输入或输出上阻塞了,就启动一个新线程来处理输入输出。不要因为网络连接或其他IO处理无法作出响应而无限期的冻结用户界面。
3.如果需要等待指定的时间,不要让事件派发线程睡眠,而应该使用定时器,只能在事件指派线程上访问 Swing 组件。
4.在线程中做的事情不能接触用户界面。在启动线程前,应该先阅读来自用户界面的信息然后再启动他们,一旦这些线程完成就从事件派发线程中更新用户界面。(此又称为Swing程序的单一线程规则不过也有一些列外:
1. 只有很少的Swing方法是线程安全的:JTextComponent.setText JTextArea.insert JTextArea.append JTextArea.replaceRange。
2. 还有JComponent类中的repaint方法和revalidate方法可以从任意线程中调用。repaint方法调度一个重绘事件。如果在构件的内容发生变化时,构件的大小和位置也都必须进行相应的更新,那么就应该使用revalidate方法。ravalidate方法将构件布局标记为无效,并调度一个布局事件(像paint事件,布局事件也是聚集的。如果事件队列中存在多个布局事件,布局只被重新计算一次)。
3. repaint使用较多但revalidate方法并不常用:revalidate主要用来在内容改变后强制执行一次构件的布局。传统的AWT也有一个validate方法强制执行一次构件的布局。对于swing构件,应该调用revalidate方法。要注意的是JFrame是一个Component而不是JComponent,因此要强制执行一次JFrame的布局应该调用validate方法。
4. 你可以在任意一个线程里安全的添加和移除一个事件监听器。(当然事件监听器的方法会在事件派发线程中被出发)。
5. 你可以构建构件,设定它们的属性,然后把它们添加到容器中,只要这些构件还没有被realized。若构件能够接收paint或validation事件了,那么就表示这个构件已经被实现了。只要在这个构件上调用了setVisible(true)或pack方法,或者构件被添加到一个已经实现的容器中,就可以满足这个条件。一旦构件realized就不能再次从另一个线程操纵它了。我们可以在main方法中在调用setVisible(true)之前创建一个应用程序的GUI,也可以在applet的构造器或init方法中创建GUI。

考虑下面的情况:假设触发一个单独的线程运行一项耗时的任务。你想通过GUI界面来表现该线程任务的进展情况,任务完成时你想再次更新GUI。但你不能从你的线程中接触到Swing构件。如:如果你想更新进度条或标签上的内容,你不能仅在你的线程中设置它的值。
为了解决此问题,在任何线程中你都可以使用两种方便有效的方法来向事件队列中添加任意的动作。例如:你想在一个线程中周期性的更新标签来表明进度,你不能从你的线程中调用label.setText。而应该使用EventQueue类的invokeLater和invokeAndWait方法使所调用的方法在事件派发线程中执行。

应该将Swing代码放入实现了Runnable接口的类的run方法中,然后创建一个该类的对象并将其传入静态的invokeLater或invokeAndWait方法。
如:
EventQueue.invokeLater(new Runnable(){
public void run(){
label.setText(percentage+”% Complete”);
}
});
当事件发布到事件队列中时,invokeLater方法立即返回,而run方法则被异步执行。invokeAndWait方法等待直到润方法确实被执行过为止。
处理更新进度标签的情况中,invokeLater方法更为适用。因为用户更希望工作器线程更快的完成工作而不是得到十分精确的进度指示器。
上述两个方法都在事件派发线程中执行而没有任何新的线程被创建。

static void invokeLater:。在等待处理的线程被处理后,使Runnable对象的run方法在事件派发线程中执行
static void invokeAndWait:在等待处理的线程被处理后,使Runnable对象的run方法在事件派发线程中执行,该调用会阻塞直到run方法终止。

Swing工作器:
当用户发布一条很费时的任务时,可以通过启动一个新线程来完成工作。就像开始介绍的线程应该使用EventQueue.invokeLater方法来更新用户界面。
SwingWorker类可以很轻松的完成这种工作。

工作器线程的典型UI行为:
1.在工作开始之前完成UI的初始化。
2.在每个工作单元之后更新UI来显示进度。
3.整个工作完成之后,对UI作出最后的更新。

Swing中的并发:
并发的小心使用对Swing编程人员是非常重要的。好的Swing程序能有效利用并发而不会导致程序被冻结–不管做什么不管何时程序总能及时响应用户接口。因此程序员要掌握Swing框架是如何使用线程的。
主要包括以下三种类型的线程的使用:
1.初始线程:用来执行程序的初始化代码。
2.事件派遣线程:执行所有的事件处理代码;大部分与Swing框架交互的代码也由该线程来执行。
3.工作者线程:也称为后台线程。用来执行耗时的后台任务。
程序员不必专门写代码来明确创建这些线程:因为它们是由运行时或Swing框架自动提供的。程序员的任务是利用好这些线程创建响应及时的可维护的Swing程序。
就像其他运行在Java平台的程序一样,Swing程序也可以创建线程和线程池。
javax.swing.SwingWorker是一个非常重要的类,他可以实现worker thread的任务和其他线程任务之间的通信并进行调节。

1.初始线程:每一个程序都有一个线程集它们是应用程序在逻辑上开始执行的地方。在一般的标准程序中:仅有一种这样的线程:调用主类中的main方法。对于Applet这些初始线程执行applet对象的构造以及调用该对象的init方法和start方法。这些初始化动作可能发生在单个线程也可能2个或3个不同的线程,这取决于Java平台的实现。

在Swing程序中,初始线程并不需做很多的事情,它们最主要的任务是创建一个Runnable对象来初始化GUI以及调度所创建的对象到event dispath thread上执行。一旦GUI被创建,程序主要由GUI事件驱动执行,GUI事件会使短小的处理代码由event dispath thread来执行。应用程序代码能调度额外的任务去event dispath 线程来执行(不过这些任务要能很快执行完,因此也不能影响事件的处理interface with event processing),也可以调度到worker thread上执行(对于那些需长时间运行的任务)。

初始线程通过javax.swing.SwingUtilities.invokeLater(仅仅调度该任务就立即返回)或javax.swing.SwingUtilities.invokeAndWait(调度该任务直到任务执行完毕才返回) 这两个方法(此两方法都有以一个Runnable对象(用来定义任务的对象)作为参数)来进行GUI的创建。
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
}
在applet中,GUI的创建任务必须由init方法中调用invokeAndWait来完成。否则可能出现在GUI还没创建好init方法就已经返回,导致浏览器在加载该applet时出现问题。然而在其他程序中对GUI的创建通常是初始线程最后才做的事情,因此使用invokeLater和invokeAndWait都是一样的。
初始线程为什么不自己简单的就创建GUI呢:那是因为用来创建Swing组件和与Swing组件进行交互的代码大部分都运行在event dispath thread之中。

事件派遣线程:Swing事件处理代码运行在event dispath thread上。大部分调用Swing方法的代码也是运行在该线程上。由于大部分Swing方法都不是线程安全的因此运行于同一线程上这是有必要的。如果从很多其他线程调用这些线程不安全的方法导致线程间相互干扰或内存不一致的错误。那些线程安全的Swing组件方法可以安全的被任何线程调用。而所有其他线程不安全的方法只能被事件分发线程调用。若忽略这个规则,可能出现功能在大部分情况下是正确的但有时会遭遇出乎意料的错误,这些错误很难重现。
也许你会很奇怪为什么Java平台如此重要的一部分不设计成线程安全的。主要是因为任何试图创建线程安全的GUI库都将面临严重的问题。

我们应该时刻谨记运行在event dispath thread上的任务需要满足短小不太耗时、能快速运行完的条件。其他大型任务可以用invokeLater或invokeAndWait方法在应用程序中调度。如果你想判断你的代码是不是运行在event dispath thread上,你可以调用javax.swing.SwingUtilities.isEventDispatchThread。

工作者线程和SwingWorker:
当一个Swing程序要执行很耗时的任务时,它通常需要使用worker threads也就是所说的background threads(后台线程)。每一个运行在worker thread上的任务都由javax.swing.SwingWorker的实例来表示。SwingWorker是一个抽象类。因此你必须定义一个继承自SwingWorker的子类来创建对象,当然匿名内部类也是一种创建形式。

SwingWorker提供了很多通信和控制的特真:
1.SwingWorker的子类能定义done方法:当后台线程完成时被event dispath thread自动的调用。
2.SwingWorker实现了java.util.concurrent.Future。该接口允许background task返回一个值给其他线程,接口中有方法取消background task以及发现是否有background task完成或被取消。
3.background task通过调用SwingWorker.publish来促使SwingWorker.process被event dispath thread调用以返回中间结果。
4.background task能定义捆绑属性。对这些属性的改变都会触发相应的事件,从而导致事件处理方法被event dispath thread所调用。
javax.swing.SwingWorker 是在JDK6.0加进来的。在此之前也有一个叫SwingWorker的类并广泛用于同一个目的。
老的SwingWorker不是Java平台规范的一部分,也没有作为JDK的一部分提供。
javax.swing.SwingWorker 完全是一个新类。在功能上它并不是严格上的老的SwingWorker的功能超集新老SwingWorker类中具有相同功能的方法的名字并不一定相同。而且旧的SwingWordker是可以复用的。javax.swing.SwingWorker 的实例对新的background task是必须的。

简单Background Tasks:
下面是一个简单的但是潜在很耗时的任务:TumbleItem applet加载一系列图像文件用于动画制作。如果图像文件由初始线程来加载,将可能在GUI显示以前出现长时间的延迟(因为GUI的初始化和显示由初始线程来完成)。如果由event dispath thread加载,GUI可能出现短暂的无法及时响应事件的情形。为了避免上述的情况,TumleItem从他的初始线程创建并执行SwingWorker的一个实例对象。该对象的doInBackground方法,在一个worker thread中执行将图像加载到一个ImageIcon数组并返回该数组的引用。然后done方法在一个event dispath thread中执行并调用get方法以获取该图像数组引用将该引用赋值给applet class的imgs成员。这样就可以让TumbleItem能快速的构造GUI而不用等待装载图像的完成。
SwingWorker worker = new SwingWorker

0 0