欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

(三)Concurrency in Swing(Swing中的并发) 博客分类: 毕业设计:我的迅雷 Swing多线程thread活动Oracle 

程序员文章站 2024-03-25 20:57:52
...

本文译自:http://download.oracle.com/javase/tutorial/uiswing/concurrency/index.html

 

Concurrency in Swing(Swing中的并发)

    本文讨论适用于Swing应用程序的并发。假设你已经对线程“并发”有所了解。(下文中,“并发”常用作名词)

    小心使用并发对于Swing程序设计者来说是很重要的。一个好的Swing程序使用并发创建绝不会“愣住”的用户界面—无论程序背后在做什么,程序总是要对用户的交互做出相应。要创建一个应答式的程序,程序员必须了解Swing框架如何使用线程。

 

    一个Swing程序员通常要处理以下几种线程:

  •  Initial threads (用于初始化的线程),  这个线程执行初始化应用程序的代码。
  •  The event dispatch thread  (EDT事件分派线程),  所有的事件处理代码在这个线程上执行。几乎所有影响Swing组件的代码也在这个线程上执行。
  •  Worker threads (工作线程), 也称为 background threads (后台线程),费时的后台任务在此线程上运行。

  程序员不需要明确地创建这些线程的代码,它们由虚拟机和Swing框架提供。程序员的工作是利用这些线程创建一些可响应、可维护的Swing程序。

  像运行在java平台的其他程序一样,Swing程序可以创建额外的线程和线程池(使用Concurrency 一节中描述的工具)。不过,对于基本Swing程序来说,下面描述的线程是非常重要的。

    一次讨论三种线程。Worker线程占据较多的篇幅,因为在其上运行的任务使用java.swing.SwingWorker创建。这个类有很多有用的功能,包括worker线程上的任务和别的线程上的任务之间的 通信 和 协作。

 

    (1)Initial Thread (初始化线程)

    (2)The Event Dispatch Thread (EDT事件分派线程)

    (3)Worker Threads and SwingWorker (工作线程 和 Swing工作者)

    (4)Simple Background Tasks (简单的后台任务)

    (5)Tasks that Have Interim Results (返回中间结果的任务)

    (6)Canceling BackGround Tasks (取消后台任务)

    (7)Bound Properties and Status Methods (绑定属性 和 状态方法)

 

(1)Initial Thread (初始化线程)

        每个程序都有一些线程用于进入程序的入口。在标准程序中,只有一个这样的线程:一个调用程序类的main方法的线程。applets的 intial threads 构造applet对象和调用它的 init 和 start 方法;这些动作发生在一个线程,或是两个三个不同的线程中,这依赖于java平台的实现。在本文中,我们称这类线程为 initial threads。

 

    在Swing程序中, intial threads 并没有太多的工作。他们大多数必要的工作是创建一个用于初始化GUI的 Runnale 对象,然后将它安排到 event dispatch thread(事件分派线程EDT) 上执行,GUI真正初始在EDT上完成。

 

    一旦GUI被创建,程序基本上受 GUI 事件所驱动,每个小 GUI 事件 触发 EDT 上的小任务的执行。 可以在程序代码中增加任务到 EDT时间分派线程(如果能快速完成) 或 增加到工作者线程 worker thread (如果任务耗时)。

 

    一个 "初始化线程" 通过调用 javax.swing.SwingUtilities.invokeLater or javax.swing.SwingUtilities.invokeAndWait 来执行 ”GUI创建任务“。这两个方法都有一个参数:Runnable。 由他们名字的不同容易知道:invokeLater简单地安排任务并返回; invokeAndWait 等待任务完成后返回。(前者异步,后者同步)

    你常在demo中看到下面代码:

 

SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        createAndShowGUI();
    }
});

 

    对于applet,创建GUI的任务一定使用 invokeAndWaite方法从 init 方法开始; 不然的话,init 方法会在 GUI 创建前返回,这回给浏览器加载applet带来一些问题。在其他程序中,执行 “GUI创建” 任务 通常是 "初始化线程" 最后做的一件事。 所以 使用 invokeLater 或 invokeAndWaite 都无关紧要。

 

    为什么 initial thread 自己不创建 GUI 呢? 因为几乎所有创建和影响Swing组件的代码都要在 EDT 上跑。 这种严格的要求在下节中讨论。

 

(2)The Event Dispatch Thread (EDT事件分派线程)

    Swing事件处理器的代码运行在一个特别的线程上,我们称之为 event dispatch thread(事件分派/指派线程)。大多数调用SwingAPI的代码都在这个线程上跑。这是必须的,因为大部分Swing的方法都是“线程不安全的”:如果在多线程中调用它们可能会遇到thread interference or memory consistency errors 的问题。一些Swing组件方法在文档中被标记未“线程安全”;这些可以被任何线程安全的调用,其他Swing组件方法都必须通过 EDT 调用。如果忽略这个规则,或许大多数时间程序运行无误,不过将会遭受不可预知且难于重现的错误。

 

      注释: 大家是否觉得怪异?java平台中如此重要的一部分居然是线程不安全的。事实证明:任何尝试创建线程安全的GUI库,将面临一些基本问题。有关这方面的内容,可参考Graham Hamilton的文章:MultiThreaded toolkits: A failed dream? (我翻译了此文,在博客中可以找到)

 

    认为“代码如一系列小任务在事件分派线程上运行”对我们是有益的。大多数任务是事件处理方法的调用,例如ActionListener.actionPerformed。其他任务通过程序代码调用 invokeLater 或 invokeAndWait 来安排执行。事件分派线程上的任务一定是比较快速完成的;如果不是的话,未处理事件会堵塞,用户接口变得无法响应。

 

    如果你需要判断当前线程是否在 EDT 上跑的话,可以调用javax.swing.SwingUtilities.isEventDispatchThread 方法。

 

(3)Worker Threads and SwingWorker 工作者线程和Swing工作者

对SwingWorker不熟悉的可以先阅读此文:http://vearn.iteye.com/blog/344591

 

    当一个Swing程序需要运行一个耗时任务的时候,它通常需要一个woker threads,也就是我们所知道的 background threads。每个工作在worker thread上的任务代表一个javax.swing.SwingWorker实例。SwingWorker类是一个抽象类;你必须定义一个子类来创建SwingWorker对象;匿名内部类是常常用于创建简单的SwingWorker对象。

    SwingWorker提供了一些通信和控制功能:

  • SwingWorker的子类可以定义一个done方法,当后台任务(background task)完成后,事件分派线程(EDT)自动调用该方法。
  • SwingWorker实现了java.util.concurrent.Future接口,该接口允许background task提供一个返回值给其他线程。该接口的其他方法允许取消background task或知道background task是否已经完成或已被取消。
  • 通过调用SwingWorker.publish,引发事件分派线程调用SwingWorker.process , 这样background task可以提供中间结果。
  • background可以定义”绑定属性“(bound properties)。改变这些属性会触发事件,引发EDT调用对应的事件处理方法.

    结合开头的那篇文章http://vearn.iteye.com/blog/344591,在看看“如何使用进度条”一文中的第一个demo,很容易理解SwingWorker的作用。

 

    提醒:java.swing.SwingWorker类收录在JAVA SE 6.比这更重要的是,另一个同样叫SwingWorker的类,基于同样的目的被广泛使用。旧的SwingWorker已经不再是java平台的一部分,也不作为 Jdk 部分。

    新的javax.swing.SwingWorker是一个全新的类。在功能上,他不是旧的SwingWorker的扩展。两个类的方法拥有同样功能却不一样的名字。同时,旧的SwingWorker实例可以重复使用,而现在,对于一个新任务则需要一个新的java.swing.SwingWorker实例。

 

 

 拷贝JDK文档中的内容:

使用 Swing 编写多线程应用程序时,要记住两个约束条件:(有关详细信息,请参阅 How to Use Threads ):

  • 不应该在事件指派线程 上运行耗时任务。否则应用程序将无响应。
  • 只能在事件指派线程 *问 Swing 组件。

 

这些约束意味着需要时间密集计算操作的 GUI 应用程序至少需要以下两个线程:1) 执行长时间任务的线程; 2) 所有 GUI 相关活动的事件指派线程 (EDT)这涉及到难以实现的线程间通信。

SwingWorker 设计用于需要在后台线程中运行长时间运行任务的情况,并可在完成后或者在处理过程中向 UI 提供更新。SwingWorker 的子类必须实现 doInBackground() 方法,以执行后台计算。

工作流

SwingWorker 的生命周期中包含三个线程:

  • 当前 线程:在此线程上调用 execute() 方法。它调度 SwingWorker 以在 worker 线程上执行并立即返回。可以使用 get 方法等待 SwingWorker 完成。

  • Worker 线程:在此线程上调用 doInBackground() 方法。所有后台活动都应该在此线程上发生。要通知 PropertyChangeListeners 有关绑定 (bound) 属性的更改,请使用 firePropertyChange getPropertyChangeSupport() 方法。默认情况下,有两个可用的绑定属性:stateprogress

  • 事件指派线程 :所有与 Swing 有关的活动都在此线程上发生。SwingWorker 调用 process done() 方法,并通知此线程的所有 PropertyChangeListener

通常,当前 线程就是事件指派线程

worker 线程上调用 doInBackground 方法之前,SwingWorker 通知所有 PropertyChangeListener 有关对 StateValue.STARTEDstate 属性更改。doInBackground 方法完成后,执行 done 方法。然后 SwingWorker 通知所有 PropertyChangeListener 有关对 StateValue.DONEstate 属性更改。

SwingWorker 被设计为只执行一次。多次执行 SwingWorker 将不会调用两次 doInBackground 方法。

//拷贝完毕

 

(4)Simple Background Tasks(简单的后台任务)

    我们从一个非常简单却可能费时的任务开始。TumbleItem applet 在播放动画中加载一组图片文件。如果图片文件在 initial 线程中加载的话, 可能延迟GUI的创建。如果图片文件是在 EDT 中加载,则GUI可能暂时无法响应。

 

    为了避免这些问题,TumbleItem 在 initial threads 中创建一个 SwingWorker 实例并运行。 这个实例对象的 doInBackground方法,在 worker thread 上运行, 把图片加载到一个 ImageIcon 数组,并返回数组引用。 而 done 方法则在 EDT 上执行, 调用 get 方法 重新获得这个引用,赋值给 applet 类的字段 imgs。这样就是的 TunbleItem快速构建 GUI,不需要等待图片加载完成。

 

    以下代码定义并运行SwingWorker对象,其中展示了如 SwingWorker 的 doInBackground 方法返回一个中间结果:

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 方法返回值的类型, get方法被其他线程调用获得一个结果,该结果是doInBackground 方法的返回值。SwingWorker 的第二个类型参数是: 当后台任务还在进行时,返回的中间结果的类型。 因为这个demo没有返回中间结果,所以 Void 被用作参数,类似占位符。(若要仔细了解,请查看JDK文档javax.swing.SwingWorker .)

 

    你可能会质疑,赋值 imgs 的代码是不必要的麻烦。 为什么 让 doInBackground 方法 返回一个对象,然后又用 done 方法重新获得它呢? 为什么不在 doInBackground 方法中直接 赋值 imgs 呢? 问题是:imgs对象的引用在 worker thread 中创建, 并在 EDT 中使用。当对象按照直接赋值这种方法来共享的话,你必须保证一个线程中的改变对于另一个线程是透明的。使用 get 方法可以确保透明, 因为使用 get 方法在 创建imgs 和 使用 imgs 之间 建立一个 hapens-before 关系(在使用前完成创建)。

 

    这里有两种实际的方式去重新获得 doInBackground 方法返回的对象:

  •   调用 无参数的 SwingWorker.get 方法。如果后台任务还未完成(doInBackground方法还没返回),get 方法 阻塞 直到 doInBackground 执行完。
  •   调用 带一个参数的 SwingWorker.get 方法, 该参数指明超时时间。 如果后台任务未完成, get 方法 阻塞 直到它完成,除非  超过了参数指定的超时时间。超过时间的话,gei方法会抛出java.util.concurrent.TimeoutException 异常。

    注意:当你在 EDT 上调用过多 get 方法是, 在 get 方法未返回前, EDT不会处理任何 GUI 事件,GUI就像冰冻了一样。 另外,如果你不能肯定后台任务能够完成或结束,请使用带参数的 get 方法。

 

 (关于happen-before的话题,我参考他人的一段话做了下面讨论

    Do not write a reference to the object being constructed in a place where another thread can see it before the object's constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object's final fields.

    不要在任何别的线程能看到的地方引用一个正在被创建的对象。如果你这么做的话,当对象被其他线程“看到”,则看到它的线程 今后都只能看到这个对象的 final字段而已)详细分析可以参考:http://www.ibm.com/developerworks/cn/java/j-jtp0618/index.html

 

(5)Tasks that Have Interim Results(有中间结果任务)

    我们常常用到:当后台任务还在进行时,后台任务提供一个中间结果。任务可以通过调用 SwingWorker.publish 来实现。这个方法接受多个参数,每个参数的类型要跟 SwingWorker 的第二个参数类一样((4)提到了)。

 

    为了收集 publish 提供的结果, 可以重写 SwingWorker.process 。这个方法将会被 EDT 调用。多个 publish 调用 通常会 合并为一个 process 调用。

 

    观察 Flipper.java 例子如何使用 publish 方法提供中间结果。这个程序通过 在后台任务 中生产一组随机的boolean值来验证 java.util.Random 是否公平。这等同于抛硬币,因此命名为Flipper。后台任务使用一个 FlipPair类型 的静态私有类 作为反馈结果。

 

private static class FlipPair {
    private final long heads, total;
    FlipPair(long heads, long total) {
        this.heads = heads;
        this.total = total;
    }
}

    heads字段是随机值为true的次数,total 是总次数。

 

    后台任务代表一个 FlipTask 实例

private class FlipTask extends SwingWorker<Void, FlipPair> {

 

    由于任务不是返回最终结果,所以 SwingWorker 的第一个类型参数无关紧要, 用Void 作占位符。 每次“抛硬币”后,任务调用 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;
}

  (isCancelled 方法在下一节讨论)因为 publish 被多次调用, 所以在 process 被 EDT 调用前,大量的 FlipPair 值将可能被合并; process 只对最后一个反馈的值有兴趣,使用它更新GUI。

protected void process(List<FlipPair> 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 方法是如文所描述的一样,是真正 “线程安全的”。这意味着,我们可以在worker 线程中指派 publish、 process 、设置文本值 这些动作。 我们忽略这个事实,只是为了给出一个简单SwingWorker中间结果的演示。

 

(6)Canceling Background Tasks(取消后台任务)

    调用 SwingWorker.cancel 可以取消一个正在运行的后台任务, 这个任务也需要配合自个的取消。这里有两种途径实现:

 

    cancel 方法带一个 boolean 类型参数,如果参数是 true, cancel 向后台任务发送一个 interrupt。可以通过调用 Thread.interrupted 方法判定是否接受了一个interrupt

	while (true) {
				if (Thread.interrupted()) {
					System.out.println("receive an interrupt");
					break;
				}
				System.out.println("still working");
			}

不管这个 cancel 方法的参数是 true 还是 false, 调用  cancel 方法 都会改变对象的 取消状态 为 true。这是 isCanceled 方法返回的值。 一旦改变, 取消状态不能回退。

 

    上一节的 Flipper 例子 使用 status-only 编码风格。 当 isCancelled 返回 true的时候, doInBackground 方法里的循环退出。 用户点击 “Cancel” 按钮 ,上面的所说的情况就会发生。

 

    对于 Flipper 应用程序,这种 status-only 方法

protected Void doInBackground() {
                        .......

			while (!isCancelled()) {
				total++;
				if (random.nextBoolean()) {
					heads++;
				}
				publish(new FlipPair(heads, total));
			}
                        ......
			return null;
		}

之所以有效的原因是: 它的 doInBackground 没有包含任何可能抛出 InterruptedException 的代码。要对 接收到的 interrupt 做出响应, 后他任务只需要简单的调用 Thread.isInterrupted(),如前面所讲。

 

    提醒:如果 get 方法在 后台任务取消后调用,则抛出java.util.concurrent.CancellationException 异常。

 

(7)Bound Properties and Status Methods(绑定属性 和 状态方法)

    SwingWorker 支持bound properties ,这有利于与其它进程通信。对于SwingWorker,两个预先绑定的属性:progress 和 state. 跟其他绑定属性一样,progress 和 state 可以在 EDT 上触发事件处理任务。

 

    通过实现一个属性改变监听器,程序可以跟踪 progress,state和别的绑定属性的变化。更多信息请参考 How to Write a Property Change Listener in Writing Event Listeners .

 

    progress绑定变量

    progress绑定变量是一个从0到100的正数。已经预先定义了setter和geter方法(SwingWorker.setProgress和SwingWorker.getProgress).

 

    ProgressBarDemo   示例使用 progress 在后台任务更新进度条,参考How to Use Progress Bars in Using Swing Components .(我的博文中有此文的翻译)

 

    state绑定变量

    state绑定变量只是SwingWorker对象在其生命周期中的状态。这个绑定属性可选值包含在一个枚举中,SwingWorker.StateValue.可能值为:

 

    PENDING:调用doInBackground 之前,对象构造期间的状态

    STARTED:调用doInBackground 的前一刻(很短时间)知道 调用done的前一刻

    DONE:剩余的状态(调用done的前一刻之后)

 

    state当前值可以通过调用 SwingWorker.getState 方法返回

 

    status method(状态方法)

    两个方法,是 Future 接口的一部分, 同样在后台任务对状态的反馈。isCancelled 方法,在(6)Canceling BackGround Tasks中讲了。还有 isDone 方法,如果任务完成,无论是正常完成还是被关闭取消而完成,都返回true。

 

 

 

Answers: Concurrency in Swing

Questions

Question 1: For each of the following tasks, specify which thread it should be executed in and why.
Answer 1:
  • Initializing the GUI. The event dispatch thread; most interactions with the GUI framework must occur on this thread.
  • Loading a large file. A worker thread. Executing this task on the event dispatch thread would prevent GUI events from being processed, "freezing" the GUI until the task is finished. Executing this task on an initial thread would cause a delay in creating the GUI.
  • Invoking javax.swing.JComponent.setFont to change the font of a component. The event dispatch thread. As with most Swing methods, it is not safe to invoke setFont from any other thread.
  • Invoking javax.swing.text.JTextComponent.setText to change the text of a component. This method is documented as thread-safe, so it can be invoked from any thread.

Question 2: One thread is not the preferred thread for any of the tasks mentioned in the previous question. Name this thread and explain why its applications are so limited.
Answer 2: The initial threads launch the first GUI task on the event dispatch thread. After that, a Swing program is primarily driven by GUI events, which trigger tasks on the event dispatch thread and the worker thread. Usually, the initial threads are left with nothing to do.

Question 3: SwingWorker has two type parameters. Explain how these type parameters are used, and why it often doesn't matter what they are.
Answer 3: The type parameters specify the type of the final result (also the return type of the doInBackground method) and the type of interim results (also the argument types for publish and process ). Many background tasks do not provide final or interim results.

Exercises

Question 1: Modify the Flipper example so that it pauses 5 seconds between "coin flips." If the user clicks the "Cancel", the coin-flipping loop terminates immediately.
Answer 1: See the source code for Flipper2 . The modified program adds a delay in the central doInBackground loop:
protected Object doInBackground() {
    long heads = 0;
    long total = 0;
    Random random = new Random();
    while (!isCancelled()) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            //Cancelled!
            return null;
        }


        total++;
        if (random.nextBoolean()) {
            heads++;
        }
        publish(new FlipPair(heads, total));
    }
    return null;
}
The try ... catch causes doInBackground to return if an interrupt is received while the thread is sleeping. Invoking cancel with an argument of true ensures that an interrupt is sent when the task is cancelled.