My Blog

成长的脚印

统计

最新评论

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<ImageIcon[], Void>() {
    @Override
    public ImageIcon[] doInBackground() {
        final ImageIcon[] innerImgs = new ImageIcon[nimgs];
        for (int i = 0; i < nimgs; i++) {
            innerImgs[i] = loadImage(i+1);
        }
        return innerImgs;
    }

    @Override
    public void done() {
        //Remove the "Loading images" label.
        animator.removeAll();
        loopslot = -1;
        try {
            imgs = get();
        } catch (InterruptedException ignore) {}
        catch (java.util.concurrent.ExecutionException e) {
            String why = null;
            Throwable cause = e.getCause();
            if (cause != null) {
                why = cause.getMessage();
            } else {
                why = e.getMessage();
            }
            System.err.println("Error retrieving file: " + why);
        }
    }
};
SwingWorker的所有具体子类不用强制实现doInBackground方法和done方法。

我们注意SwingWorker是一个泛型类带有两个类型参数。第一个类型参数指定了doInBackground方法和get方法(由其他线程调用以获取doInBackground方法返回的对象)的返回值类型。第二个类型参数指定了background task仍然活动时返回的临时值的类型。在此例中由于没有返回临时值所以使用void来替代。
你可能想知道上述代码中设置imgs的方法是否引入了不必要的复杂性,为什么要采用doInBackground方法返回一个对象,再通过调用done方法来获取它?为什么不直接在doInBackground方法中设置imgs。其实问题是imgs所引用的对象是在worker thread中创建,在event dispath thread中被使用;当一个对象在多个线程间共享时,你必须保证在一个线程中对该对象所做的修改要对其他线程都是可见的。采用get方法就能保证这样,因为使用get方法就在创建imgs(对象)的代码和使用imgs对象的代码之间创建了一中偏序关系(happens before relationship)。For more on the happens before relationship, refer to Memory Consistency Errors in the Concurrency lesson。
实际上有两种方式来获取doInBackground方法返回的对象:
1.调用SwingWorker.get方法(无任何参数)。如果background task没有完成,get会阻塞直到后台任务完成。
2.调用SwingWorker.get方法(带指定超时时间参数)。如果background task没有完成,get会阻塞直到后台任务完成--除非首先到达超时时间这种情况get方法会抛出 java.util.concurrent.TimeoutException。

当在事件派发线程调用任意一个get方法时都要十分小心:因为在get方法返回以前任何GUI事件都不会被处理,GUI处于冻结状态。
不要调用没有参数的get方法,除非你很有信心background task已经完成或接近完成。
在此 SwingWorker 完成之前,在事件指派线程 上调用 get 将阻塞所有 事件(包括 repaint)的处理。
想在事件指派线程 上阻塞 SwingWorker 时,建议使用 模式对话框(modal dialog)。

例如:

class SwingWorkerCompletionWaiter extends PropertyChangeListener {
private JDialog dialog;
 
public SwingWorkerCompletionWaiter(JDialog dialog) {
  this.dialog = dialog;
     }
 
public void propertyChange(PropertyChangeEvent event) {
 if ("state".equals(event.getPropertyName())
     && SwingWorker.StateValue.DONE == event.getNewValue()) {
  dialog.setVisible(false);
  dialog.dispose();
         }
     }
 }
  JDialog dialog = new JDialog(owner, true);
  swingWorker.addPropertyChangeListener(new SwingWorkerCompletionWaiter(dialog));
  swingWorker.execute();
  //the dialog will be visible until the SwingWorker is done
  dialog.setVisible(true);
 
具有临时结果的任务:
当background task在其运行期间提供临时结果通常是很有益的。在其运行时可以通过调用SwingWorker.publish做到。publish方法接收一系列的参数。每个参数的类型必须和SwingWorker指定的第二个参数类型一致。
为了收集publish方法提供的结果,需重载SwingWorker.process方法,该方法会在事件分发线程中被调用。从多个publish调用中返回的结果通常被积累到一个process调用中。
让我们看看Flipper.java使用publish提供临时结果的方式。该程序通过在后台线程中生成一系列的随机布尔值来测试Random类的fairness。这相当于抛硬币。为了报告他的结果,后台线程使用一个FlipPair类型的对象:
private static class FlipPair {
    private final long heads, total;
    FlipPair(long heads, long total) {
        this.heads = heads;
        this.total = total;
    }
}
heads域是产生的随机值为true的次数;total域是总的随机数的个数。background task 由一个FlipTask实例表示:
private class FlipTask extends SwingWorker<Void, FlipPair> {
因为该任务不返回最终结果,因此第一个参数是什么类型都无关紧要;void仅仅用作占位符。在每一个coin flip之后后台任务调用publish:
@Override
protected Void doInBackground() {
    long heads = 0;
    long total = 0;
    Random random = new Random();
    while (!isCancelled()) {
        total++;
        if (random.nextBoolean()) {
            heads++;
        }
        publish(new FlipPair(heads, total));
    }
    return null;
}
因为调用publish方法非常频繁,在process方法被事件分发线程调用之前可能会有大量的FlipPair类型的值被积累。process方法仅仅对每次reported的最后一个值感兴趣,因为要使用他类更新GUI。
protected void process(List pairs) {
    FlipPair pair = pairs.get(pairs.size() - 1);
    headsText.setText(String.format("%d", pair.heads));
    totalText.setText(String.format("%d", pair.total));
    devText.setText(String.format("%.10g",
            ((double) pair.heads)/((double) pair.total) - 0.5));
}
如果random是公平的,随着Flipper的运行devText中的值会越来越接近0.注意Flipper中的setText方法实际上正如规范中介绍的一样是线程安全的。那意味着我们可以摒弃publish和process方法直接在worker thread中设置text域的值。我们忽略此特性是为了提供一个简单的SwingWorker临时结果的示例。

取消background tasks:
为了取消正在运行的background task,可以调用SwingWorker.cancel同时该task必须与它自己的取消动作协作来完成。有两种方式来达到目的:
1.当他收到一个interrupt时进行终结。该过程在Interrupts in Concurrecy中进行了描述。
2.通过经常调用SwingWorker.isCanceled方法。如果cancel方法已经被调用则该方法返回true。
cancel方法只有一个boolean参数。如果该参数是true,cacel方法向background task发送一个中断。不管该参数是true还是false,对cancel方法的调用会使该对象的取消状态为true。这就是isCanceled方法返回的值。一旦cancellation status改变了,就不能改回来。
前一节的Flipper示例中使用了the status-only 语法。当isCancelled方法返回true时,doInBackground方法中的主循环将退出。这将发生在用户点击Cancel按钮从而触发调用cancel方法(带有一个false参数)的代码。
Flipper类采用status-only的方式是很合理的,因为对SwingWorker.doInBackground 的实现没有包含任何可能抛出InterruptedException异常的代码。为了响应中断,background task必须要常常调用Thread.isInterrupted方法。他的目的很简单就跟调用SwingWorker.isCancelled方法一样。
注意:如果get在background task被取消以后由SwingWorker对象调用的话将抛出java.util.concurrent.CancellationException 异常。
捆绑属性和状态方法:
SwingWorker支持Bound Properties(对与其他线程进行通信很有用)。两个Bound Properties已经预定义好了:prrogress和state。正如所有的bound properties,progress和state能用来触发事件分发线程中的事件处理任务。通过实现一个property change listener程序可以跟踪progress、state以及其他bound properties的变化。
progress:progress约束变量是一个从0到100的int变量。他有一个预定义的setter方法(protected SwingWorker.setProgress)和一个预定义的getter方法(public SwingWorker.getProgress);
ProgressBarDemo示例中在background task中使用progress来控制更新ProgressBar。
state:state约束变量指出SwingWorker对象在生命周期中的哪个位置。该约束变量包含有以枚举类型的SwingWorker.StateValue其可能的取值是:
1.PENDING:从对象被构造直到doInBackground方法被调用之前的这段时间。
2.STARTED:表示从doInBackground方法被调用前的不久时间到done方法被调用前的不久时间。
3.DONE:该对象存在的余下时间。
state的当前值由SwingWorker.getState方法返回

Status方法:有两个方法:Future接口的一部分,用来报告background task的状态。正如我们在Cancelling Background Tasks看到的,isCancelled返回true如果gaitask已经被取消。还有,isDone返回true如果task已经正常的完成或被取消。

posted on 2008-06-12 21:32 永不停歇的追梦者 阅读(2732) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航: