Java的Swing编程中使用SwingWorker线程模式及顶层容器

使用SwingWorker线程模式

谨慎地使用并发机制对Swing开发人员来说非常重要。一个好的Swing程序使用并发机制来创建不会失去响应的用户接口-不管是什么样的用户交互,程序总能够对其给出响应。创建一个有响应的程序,开发人员必须学会如何在Swing框架中使用多线程。
一个Swing开发人员将会与下面几类线程打交道:
(1)Initial threads(初始线程),此类线程将执行初始化应用代码。
(2)The event dispatch thread(事件派发线程),所有的事件处理代码在这里执行。大多数与Swing框架交互的代码也必须执行这个线程。
(3)Worker threads(工作线程),也称作background threads(后台线程),此类线程将执行所有消耗时间的任务。
开发人员不需要在代码中显式的创建这些线程:它们是由runtime或Swing框架提供的。开发人员的工作就是利用这些线程来创建具有响应的,持久的Swing程序。
如同所有其他在Java平台上运行的程序,一个Swing程序可以创建额外的线程和线程池,这需要使用本文即将介绍的方法。本文将介绍以上这三种线程。工作线程的讨论将涉及到使用javax.swing.SwingWorker类。这个类有许多有用的特性,包括在工作线程任务与其他线程任务之间的通信与协作。
1.初始线程
每个程序都会在应用逻辑开始时生成一系列的线程。在标准的程序中,只有一个这样的线程:这个线程将调用程序主类中的main方法。在applet中初始线程是applet对象的构造子,它将调用init方法;这些actions可能在一个单一的线程中执行,或在两个或三个不同的线程中,这些都依据Java平台的具体实现。在本文中,我们称这类线程为初始线程(initial threads)。
在Swing程序中,初始线程没有很多事情要做。它们最基本的任务是创建一个Runnable对象,用于初始化GUI以及为那些用于执行事件派发线程中的事件的对象编排顺序。一旦GUI被创建,程序将主要由GUI事件驱动,其中的每个事件驱动将引起一个在事件派发线程中事件的执行。程序代码可以编排额外的任务给事件驱动线程(前提是它们会被很快的执行,这样才不会干扰事件的处理)或创建工作线程(用于执行消耗时间的任务)。
一个初始线程编排GUI创建任务是通过调用javax.swing.SwingUtilities.invokeLater或javax.swing.SwingUtilities.invokeAndWait。这两个方法都带有一个唯一的参数:Runnable用于定义新的任务。它们唯一的区别是:invokerLater仅仅编排任务并返回;invokeAndWait将等待任务执行完毕才返回。
看下面示例:

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

在applet中,创建GUI的任务必须被放入init方法中并且使用invokeAndWait;否则,初始过程将有可能在GUI创建完之前完成,这样将有可能出现问题。在其他的情况下,编排GUI创建任务通常是初始线程中最后一个被执行的,所以使用invokeLater或invokeAndWait都可以。
为什么初始线程不直接创建GUI?因为几乎所有的用于创建和交互Swing组件的代码必须在事件派发线程中执行。这个约束将在下文中讨论。
 2.事件派发线程
Swing事件的处理代码在一个特殊的线程中执行,这个线程被称为事件派发线程。大部分调用Swing方法的代码都在这个线程中被执行。这样做是必要的,因为大部分Swing对象是“非线程安全的”。
可以将代码的执行想象成在事件派发线程中执行一系列短小的任务。大部分任务被事件处理方法调用,诸如ActionListener.actionPerformed。其余的任务将被程序代码编排,使用invokeLater或invokeAndWait。在事件派发线程中的任务必须能够被快速执行完成,如若不然,未经处理的事件被积压,用户界面将变得“响应迟钝”。
如果你需要确定你的代码是否是在事件派发线程中执行,可调用javax.swing.SwingUtilities.isEventDispatchThread。
 3.工作线程与SwingWorker
当一个Swing程序需要执行一个长时间的任务,通常将使用一个工作线程来完成。每个任务在一个工作线程中执行,它是一个javax.swing.SwingWorker类的实例。SwingWorker类是抽象类;你必须定义它的子类来创建一个SwingWorker对象;通常使用匿名内部类来这做这些。
SwingWorker提供一些通信与控制的特征:
(1)SwingWorker的子类可以定义一个方法,done。当后台任务完成的时候,它将自动的被事件派发线程调用。
(2)SwingWorker类实现java.util.concurrent.Future。这个接口允许后台任务提供一个返回值给其他线程。该接口中的方法还提供允许撤销后台任务以及确定后台任务是被完成了还是被撤销的功能。
(3)后台任务可以通过调用SwingWorker.publish来提供中间结果,事件派发线程将会调用该方法。
(4)后台任务可以定义绑定属性。绑定属性的变化将触发事件,事件派发线程将调用事件处理程序来处理这些被触发的事件。
4.简单的后台任务
下面介绍一个示例,这个任务非常简单,但它是潜在地消耗时间的任务。TumbleItem applet导入一系列的图片文件。如果这些图片文件是通过初始线程导入的,那么将在GUI出现之前有一段延迟。如果这些图片文件是在事件派发线程中导入的,那么GUI将有可能出现临时无法响应的情况。
为了解决这些问题,TumbleItem类在它初始化时创建并执行了一个StringWorker类的实例。这个对象的doInBackground方法,在一个工作线程中执行,将图片导入一个ImageIcon数组,并且返回它的一个引用。接着done方法,在事件派发线程中执行,得到返回的引用,将其放在applet类的成员变量imgs中。这样做可以允许TumbleItem类立刻创建GUI,而不必等待图片导入完成。
下面的示例代码定义和实现了一个SwingWorker对象。

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的返回值。第二个类型参数指定中间结果的类型,这个例子没有返回中间结果,所以设为void。
使用get方法,可以使对象imgs的引用(在工作线程中创建)在事件派发线程中得到使用。这样就可以在线程之间共享对象。
实际上有两个方法来得到doInBackground类返回的对象。
(1)调用SwingWorker.get没有参数。如果后台任务没有完成,get方法将阻塞直到它完成。
(2)调用SwingWorker.get带参数指定timeout。如果后台任务没有完成,阻塞直到它完成-除非timeout期满,在这种情况下,get将抛出java.util.concurrent.TimeoutException。
5.具有中间结果的任务
让一个正在工作的后台任务提供中间结果是很有用处的。后台任务可以调用SwingWorker.publish方法来做到这个。这个方法接受许多参数。每个参数必须是由SwingWorker的第二个类型参数指定的一种。
可以覆盖(override)SwingWorker.process来保存由publish方法提供的结果。这个方法是由事件派发线程调用的。来自publish方法的结果集通常是由一个process方法收集的。
我们看一下Filpper.java提供的实例。这个程序通过一个后台任务产生一系列的随机布尔值测试java.util.Random。就好比是一个投硬币试验。为了报告它的结果,后台任务使用了一个对象FlipPair。

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

heads表示true的结果;total表示总的投掷次数。
后台程序是一个FilpTask的实例:
private class FlipTask extends SwingWorker<Void, FlipPair> {
因为任务没有返回一个最终结果,这里不需要指定第一个类型参数是什么,使用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;
}

由于publish时常被调用,许多的FlipPair值将在process方法被事件派发线程调用之前被收集;process仅仅关注每次返回的最后一组值,使用它来更新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));
}

 6.取消后台任务
调用SwingWorker.cancel来取消一个正在执行的后台任务。任务必须与它自己的撤销机制一致。有两个方法来做到这一点:
(1)当收到一个interrupt时,将被终止。
(2)调用SwingWorker.isCanceled,如果SwingWorker调用cancel,该方法将返回true。
 7.绑定属性和状态方法
SwingWorker支持bound properties,这个在与其他线程通信时很有作用。提供两个绑定属性:progress和state。progress和state可以用于触发在事件派发线程中的事件处理任务。
通过实现一个property change listener,程序可以捕捉到progress,state或其他绑定属性的变化。
 
7.1The progress Bound Variable
Progress绑定变量是一个整型变量,变化范围由0到100。它预定义了setter (the protected SwingWorker.setProgress)和getter (the public SwingWorker.getProgress)方法。
 
7.2The state Bound Variable
State绑定变量的变化反映了SwingWorker对象在它的生命周期中的变化过程。该变量中包含一个SwingWorker.StateValue的枚举类型。可能的值有:
(1)PENDING
这个状态持续的时间为从对象的建立知道doInBackground方法被调用。
(2)STARTED
这个状态持续的时间为doInBackground方法被调用前一刻直到done方法被调用前一刻。
(3)DONE
对象存在的剩余时间将保持这个状态。
需要返回当前state的值可调用SwingWorker.getState。
 
7.3Status Methods
两个由Future接口提供的方法,同样可以报告后台任务的状态。如果任务被取消,isCancelled返回true。此外,如果任务完成,即要么正常的完成,要么被取消,isDone返回true。

使用顶层容器

Swing提供3种顶层容器类:JFrame,JDialog,JApplet。当使用这三个类时,你必须注意以下几点:
 
(1).为了显示在屏幕上,每个GUI组件必须是包含层次(containment hierarchy)的一部分。包含层次是组件的一个树型结构,最顶层的容器是它的根。

(2).每个GUI组件只能被包含一次。如果一个组件已经在一个容器中,这时试图将它加入到一个新的容器,则这个组件会从第一个容器移除,并加入到第二个容器中。

(3).每个顶层容器都有一个内容面板(content pane),一般情况下,这个内容面板会包含(直接或间接地)所有顶层容器GUI的可视组件。

(4).可以在顶层容器中加入一个菜单条(menu bar)。通常这个菜单条被放置在顶层容器中,但在内容面板外。

1.顶层容器与包含层次
每个使用Swing组件的程序都至少有一个顶层容器。这个顶层容器是包含层次的根节点—这个层次会包含所有将在这个顶层容器中出现的Swing组件。
    通常情况下,一个单独的基于Swing GUI的应用程序至少有一个包含层次,且它的根节点是JFrame。举例来说,如果一个应用程序拥有一个窗口和两个对话框,那么这个应用程序将会有三个包含层次,也即会有三个顶层容器。一个包含层次将JFrame作为它的根节点,两外两个包含层次各有一个JDialog作为它的根节点。
一个基于Swing组件的小程序(applet)至少含有一个包含层次,并且可以确定其中必有一个是以JApplet作为其根节点的。例如,一个小程序带有一个对话框,则它会有两个包含层次。在浏览器窗口中的组件将会置于一个包含层次,它的根节点是一个JApplet对象。对话框会有一个包含层次,它的根节点是一个JDialog对象。
 
2.将组件加入到内容面板中
下面的代码操作是上面的例子中得到frame的内容面板并加入黄色标签:
frame.getContentPane().add(yellowLabel, BorderLayout.CENTER);
 
如代码所示,必须先找到顶层容器的内容面板,通过方法getContentPane实现。默认的内容面板是一个简单的中间容器,它继承自JComponent,使用一个BorderLayout作为它的面板管理器。
定制一个内容面板很简单—设置面板管理器或添加边框。这里必须注意,getContentPane方法将返回一个Container对象,而不是JComponent对象。这意味着如果需要利用JComponent的部分功能,还必须将返回值进行类型转换或创建你自己的组件来作为内容面板。我们的实例通常采用的是第二种方式. 因为第二种方法比较清楚明朗。 另一种我们有时会使用的方法就是简单地将一个自己定义组件添加进内容面板, 完全遮盖住内容面板。
如果你创建你自己的内容面板, 那么请注意确认它是不透明的. 一个不透明的JPanel将是一个不错的选择. 注意, 默认情况下JPanel的布局管理为FlowLayout, 你或许会想要用其它的布局管理器替换它。
为了使一个组件成为内容面板, 你需要使用顶层容器的setContentPane方法, 例如:

//Create a panel and add components to it.
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setBorder(someBorder);
contentPane.add(someComponent, BorderLayout.CENTER);
contentPane.add(anotherComponent, BorderLayout.PAGE_END);
//Make it the content pane.
//contentPane.setOpaque(true);
topLevelContainer.setContentPane(contentPane);

注意: 不要使用透明的容器作为内容面板, 如JScrollPane, JSplitPane和JTabbedPane. . 一个透明的内容面板将导致组件混乱. 尽管你可以使任何的透明的Swing组件通过setOpaque(true)方法来使其不透明化, 但当一些组件被设置成完全不透明后看上去会不太对劲. 例如, 一个标签面板.
 
3.添加一个菜单栏 (Adding a Menu Bar)
从理论上来讲每一个顶层容器都可以有一个菜单栏. 但事实表明菜单栏仅出现于Frame或者Applet中. 为达到添加一个菜单栏到顶层容器, 你需要创建一个JMenuBar对象, 组装上一些菜单, 然后呼叫setJMenuBar方法. TopLevelDemo实例通过以下代码添加一个菜单栏到它的Frame中.

frame.setJMenuBar(cyanMenuBar);

4.根容器 (The Root Pane)
每个顶层容器都依赖于一个隐式的称为根容器的中间容器. 这个根容器管理着内容面板和菜单栏, 并且连同两个或者两个以上的其它容器(见图中Layered Pane等). 你通常不需要了解关于使用Swing组件根容器方面的知识.  然而, 如果你想截获鼠标的点击或者在多重组件上进行绘画动作, 那么你需要知晓根容器.

上文已经讲述了关于内容面板与可选的菜单栏的内容,此处不再复述. 根容器中包含的另外两个组件, 是布局面板和玻璃面板. 布局面板直接包含菜单栏和内容面板, 并且允许你对所添加的其它组件进行Z坐标排序. 玻璃面板通常用来截获发生在顶层中的输入动作, 并且同样可以用来在多重组件上进行绘画.

(0)

相关推荐

  • Java容器HashMap与HashTable详解

    1.HashMap HashMap继承抽象类AbstractMap,实现接口Map.Cloneable, Serializable接口.HashMap是一种以键值对存储数据的容器, 由数组+链表组成,其中key和value都可以为空,key的值唯一.HashMap是非线程安全的, 对于键值对<Key,Value>, HashMap内部会将其封装成一个对应的Entry<Key,Value>对象.HashMap的存储空间大小是可以动态改变的: 存储过程 每个对象都有一个对应的HashC

  • Java开发中的容器概念、分类与用法深入详解

    本文实例讲述了Java开发中的容器概念.分类与用法.分享给大家供大家参考,具体如下: 1.容器的概念 在Java当中,如果有一个类专门用来存放其它类的对象,这个类就叫做容器,或者就叫做集合,集合就是将若干性质相同或相近的类对象组合在一起而形成的一个整体 2.容器与数组的关系 之所以需要容器: ① 数组的长度难以扩充 ② 数组中数据的类型必须相同 容器与数组的区别与联系: ① 容器不是数组,不能通过下标的方式访问容器中的元素 ② 数组的所有功能通过Arraylist容器都可以实现,只是实现的方式不

  • 基于Java并发容器ConcurrentHashMap#put方法解析

    jdk1.7.0_79 HashMap可以说是每个Java程序员用的最多的数据结构之一了,无处不见它的身影.关于HashMap,通常也能说出它不是线程安全的.这篇文章要提到的是在多线程并发环境下的HashMap--ConcurrentHashMap,显然它必然是线程安全的,同样我们不可避免的要讨论散列表,以及它是如何实现线程安全的,它的效率又是怎样的,因为对于映射容器还有一个Hashtable也是线程安全的但它似乎只出现在笔试.面试题里,在现实编码中它已经基本被遗弃. 关于HashMap的线程不

  • 用java的spring实现一个简单的IOC容器示例代码

    要想深入的理解IOC的技术原理,没有什么能比的上我们自己实现它.这次我们一起实现一个简单IOC容器.让大家更容易理解Spring IOC的基本原理. 这里会涉及到一些java反射的知识,如果有不了解的,可以自己去找些资料看看. 注意 在上一篇文章,我说,启动IOC容器时,Spring会将xml文件里面配置的bean扫描并实例化,其实这种说法不太准确,所以我在这里更正一下,xml文件里面配置的非单利模式的bean,会在第一次调用的时候被初始化,而不是启动容器的时候初始化.但是我们这次要做的例子是容

  • 浅析Java的Spring框架中IOC容器容器的应用

    Spring容器是Spring框架的核心.容器将创建对象,它们连接在一起,配置它们,并从创建到销毁管理他们的整个生命周期.在Spring容器使用依赖注入(DI)来管理组成应用程序的组件.这些对象被称为Spring Beans. 容器获得其上的哪些对象进行实例化,配置和组装通过阅读提供的配置元数据的说明.配置元数据可以通过XML,Java注释或Java代码来表示.下面的图是Spring如何工作的高层次图. Spring IoC容器是利用Java的POJO类和配置元数据的产生完全配置和可执行的系统或

  • 迅速掌握Java容器中常用的ArrayList类与Vector类用法

    ArrayList类 List集合的实例化: List<String> l = new ArrayList<String>(); //使用ArrayList类实例化List集合 List<String> l2 = new LinkedList<String>(); //使用LinkedList类实例化List集合 ArrayList常用方法: add(int index, Object obj); addAll(int, Collection coll);

  • java并发容器CopyOnWriteArrayList实现原理及源码分析

    CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet.本文会对CopyOnWriteArrayList的实现原理及源码进行分析. 实现原理 我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,

  • Java多线程编程中的两种常用并发容器讲解

    ConcurrentHashMap并发容器 ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁. ConcurrentHashMap的内部结构 ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下Con

  • 深入理解Java的Spring框架中的IOC容器

    Spring IOC的原型 spring框架的基础核心和起点毫无疑问就是IOC,IOC作为spring容器提供的核心技术,成功完成了依赖的反转:从主类的对依赖的主动管理反转为了spring容器对依赖的全局控制. 这样做的好处是什么呢? 当然就是所谓的"解耦"了,可以使得程序的各模块之间的关系更为独立,只需要spring控制这些模块之间的依赖关系并在容器启动和初始化的过程中将依据这些依赖关系创建.管理和维护这些模块就好,如果需要改变模块间的依赖关系的话,甚至都不需要改变程序代码,只需要将

  • java容器详细解析

    前言:在java开发中我们肯定会大量的使用集合,在这里我将总结常见的集合类,每个集合类的优点和缺点,以便我们能更好的使用集合.下面我用一幅图来表示 其中淡绿色的表示接口,红色的表示我们经常使用的类. 1:基本概念 Java容器类类库的用途是保存对象,可以将其分为2个概念. 1.1:Collection 一个独立元素的序列,这些元素都服从一条或多条规则.其中List必须按照插入的顺序保存元素.Set不能有重复的元素.Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同) 1.2:Ma

  • Java容器类的深入理解

    Java容器类包含List.ArrayList.Vector及map.HashTable.HashMap ArrayList和HashMap是异步的,Vector和HashTable是同步的,所以Vector和HashTable是线程安全的,而ArrayList和HashMap并不是线程安全的.因为同步需要花费机器时间,所以Vector和HashTable的执行效率要低于ArrayList和HashMap.Collection├List       接口│├LinkedList       链表

  • 深入理解Java线程编程中的阻塞队列容器

    1. 什么是阻塞队列? 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列.这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空.当队列满时,存储元素的线程会等待队列可用.阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程.阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素. 阻塞队列提供了四种处理方法: 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException("Q

随机推荐