使用数据泵进行数据导入和导出
摘自:http://www.oracle.com/global/cn/oramag/oracle/04-sep/o54data.html
找到将数据从仓库迁移到集市的最快方法。
Lora是Acme银行的数据库管理员,她现在在该银行高层管理团队高级会议上成了大家最关注的核心人物。这次会议的目的是确定一些方法,来使最终用户能够详细分析公司主数据仓库中的数据。会上提出的一种想法是创建几个小型数据集市--每个集市根据一个特定的职能范围存储数据--这样每个数据集市就可以由专门的团队来使用。
为了有效地实现数据集市的方法,数据专家必须能将数据快速、有效地放入数据集市中。该团队面临的挑战就是解决如何用数据仓库中的数据快速刷新数据集市中的数据,而这些数据集市又运行在各个结构不同的平台上。这就是Lora为什么出席会议的原因。她会为移动数据提出哪些可供选择的方法呢?
作为一名经验丰富、知识渊博的数据库管理员,Lora向与会者提供了三种可能的方法,分别是:
本文介绍Lora对这三种可选方法的解释,包括它们的实施细节和优缺点。
可移动表空间
Lora从可移动表空方法开始介绍。把整个表空间移动到目标系统的最快速方法是用FTP(文件传输协议)或rcp(远程复制)来简单地转移表空间的基本文件。但是,仅仅复制Oracle数据文件还不够,目标数据库必须识别出并导入文件以及相应的表空间,最终用户才能使用表空间数据。使用可移动表空间包括复制表空间文件和使它们中的数据在目标数据库中可用。
在考虑该方法之前必须进行一些审查。首先,对于要转移到目标系统的表空间TS1,它必须是自含式的(self-contained)。也就是说,在该表空间中表的所有索引、分区及其他从属于该表的各数据段都必须在该表空间内部。Lora解释说,如果一个表空间集合包含所有从属的数据段,那么就认为这个集合是自含式的。例如,如果表空间TS1和TS2要作为一个集合进行转移,TS1中的一个表在TS2中有一个索引,则这个表空间集合就是自含式的。但是,如果TS1中的一个表另一个索引在表空间TS3中,则该表空间集合 (TS1, TS2)就不是自含式的。
要移动表空间,Lora提议使用Oracle数据库10g中的数据泵导出(Data Pump Export)工具。数据泵是Oracle的新一代数据转移工具,它替换了早期的Oracle Export (EXP)和Import (IMP)工具。这些老的工具使用正则SQL来提取和插入数据,而数据泵则与它们不同,它使用能绕过SQL缓冲区的专用API,从而使操作过程速度变得极快。此外,数据泵可以提取特定的对象,如特定的存储过程或特定表空间的表集合。 数据泵的导出和导入可以由作业控制,数据库管理员可以随时暂停、重启或终止这些作业。
开会前Lora运行了一项测试,看看数据泵能否解决Acme的要求。Lora进行的测试是转移TS1和TS2表空间,步骤如下:
1.检查TS1和TS2这个表空间集合是否是自含式的。执行下面的命令:
BEGIN
SYS.DBMS_TTS.TRANSPORT_SET_CHECK ('TS1','TS2');
END;
2.确定所有不可移动的集合。如果没有选择任何行,则该表空间是自含式的:
SELECT * FROM TRANSPORT_SET_VIOLATIONS; no rows selected
3.确保该表空间是只读的:
SELECT STATUS
FROM DBA_TABLESPACES
WHERE TABLESPACE_NAME IN ('TS1','TS2');
STATUS
---------
READ ONLY
READ ONLY
4.使用传输机制,如FTP或rcp,将每个表空间中的数据文件移到远程系统,放到/u01/oradata目录下。
5.在目标数据库中,创建一个到源数据库的数据库链接(在下面的命令行中命名为srcdb)。
CREATE DATABASE LINK srcdb USING 'srcdb';
6.在目标数据库中,使用数据泵导入工具将该表空间导入到该数据库中。
impdp lora/lora123 TRANSPORT_DATAFILES= "'/u01/oradata/ts1_1.dbf', '/u01/oradata/ts2_1.dbf'" NETWORK_LINK='srcdb' TRANSPORT_TABLESPACES=\(TS1,TS2\) NOLOGFILE=Y
这一步就使TS1和TS2表空间以及它们的数据可以在目标数据库中可用。
请注意,Lora并没有从源数据库导出元数据。她只是在上面的impdp命令中指定参数NETWORK_LINK的值为srcdb,即到源数据库的数据库链接。数据泵导入工具通过数据库链接从源数据库中获得所需的元数据,并在目标数据库中重新创建它们。
7. 最后,使源数据库中的TS1和TS2表空间成为可读写。
ALTER TABLESPACE TS1 READ WRITE; ALTER TABLESPACE TS2 READ WRITE;
这一步使TS1和TS2表空间在源数据库中对用户立即可用。
请注意,在前面讲的所有步骤中,最费时的是第4步,在这一步中要跨各系统移动数据文件。
跨不同平台的挑战
Lora知道,数据复制过程中的复杂因素之一就是源平台和目标平台经常不一致。例如,在Acme的环境中,当前数据仓库位于运行Tru64 UNIX的HP服务器上,而建议的数据集市将部署在运行Linux和Windows的Intel硬件上。
在Oracle数据库10g出现之前,在Acme使用可移动表空间不是一个切实可行的方法。你不能移动表空间,除非源数据库和目标数据库运行在同一个平台上。
有了Oracle数据库10g,这一限制得到了极大的缓解。在移动表空间时,数据文件可以跨各操作系统任意复制。在前面的例子中,数据文件可以从Tru64 UNIX复制到Linux或Windows,而目标数据库仍能识别它们。
但是,管理团队必须考虑另一种限制。默认情况下,只有两个操作系统具有相同的字节顺序(也被称为"endian-ness")时才可能进行跨操作系统复制。在Acme的情况下,Tru64 UNIX、基于Intel的Linux和Windows都使用little-endian字节顺序(低位在前),因此在它们之间进行文件复制是可能的。但是,有的团队成员提出了在有关数据仓库/数据集市的提案中使用Solaris操作系统的想法。Solaris使用big-endian字节顺序(高位在前),这使得在数据库之间进行简单的文件复制成为不可能。
Lora解释说,Oracle提供了使用Oracle Recovery Manager (RMAN)进行字节顺序转换的解决方案。她描述了针对RMAN解决方案的一个测试,该测试根据前面讲到的移动表空间解决方案的7个步骤,并稍作修改来适应Solaris到Intel/Linux的环境。除了在第5步之前或之后额外增加一步以外,所有操作步骤都一样。在源(Solaris)数据库服务器上执行的额外这一步如代码清单1中所示。
用这些代码,可以从Solaris格式的/u01/oradata/ts1_01.dbf文件创建Intel Linux格式的文件/u01/tts/ TS1_34。注意最初的文件并没有被破坏;只是创建了一个可以被导入和传输到Linux上的目标数据库中的新文件。
然后Lora讨论了对RMAN解决方案的一些修改。第一,为了提高性能,她可以指定PARALLELISM=<degree>子句来提高执行线程的数目。第二,她可以指定在不同的目录下以相同的文件名创建数据文件。这些修改如下:
RMAN> CONVERT TABLESPACE TS1 2> TO PLATFORM 'Linux IA (32-bit)' 3> DB_FILE_NAME_CONVERT 4> '/u01/oradata','/u01/tts' 5> PARALLELISM=4 6> ;
这条命令用相同的文件名ts1_01.dbf但在/u01/tts目录下根据原始文件/u01/oradata/ts1_01.dbf创建一个转换后的数据文件。这种方法将所有转换后的文件放到一个位置,这样确定转移哪个文件就更简单了。
Lora还可以在目标(Linux)数据库服务器而不是在源服务器上执行变换。在这种情况下,将在Linux服务器上执行转换,如下所示:
RMAN> CONVERT DATAFILE 2> '/u01/oradata/ts1_1.dbf', '/u01/oradata/ts2_1.dbf' 3> TO PLATFORM='Linux IA (32-bit)' 4> FROM PLATFORM='Solaris[tm] OE (64-bit)' 5> DB_FILE_NAME_CONVERT="ts","tslinux" 6> ;
这条命令通过用tslinux代替ts(换句话说,将ts1_1.dbf转换到Linux上的文件格式,新文件名为tslinux1_1.dbf)来创建文件。在数据库内,文件将用这个文件名。
Lora怎么知道哪个平台用哪种字节顺序呢?在数据字典视图上执行以下查询就会给出答案:
SELECT * FROM V$TRANSPORTABLE_PLATFORM ORDER BY PLATFORM_ID;
数据泵导出与导入工具
选择可移动表空间的限制之一是在转移文件时源表空间必须是只读模式。在现实世界中,并不总是能满足这一要求。例如,在OLTP数据库中,可能对表要经常进行读写操作。
Lora提出的另一种方法是使用Oracle数据库10g中的数据泵实用工具来转移表空间。她对这种方法进行的测试包括移动TS1和TS2表空间内容的以下步骤:
1.创建一个目录对象来存放转储的文件。
CREATE DIRECTORY dump_dir AS '/u01/dumps';
2.用数据泵导出工具导出数据。
expdp lora/lora123 TABLESPACES=\(ts1,ts2\) DUMPFILE=ts1_ts2.dmp DIRECTORY=dump_dir
这一步创建一个包含TS1和TS2表空间内容的文件/u01/dumps/ ts1_ts2.dmp。
3.将文件ts1_ts2.dmp转移到远程系统中,放在目录/u01/dumps下(用文件传输的方法如FTP或rcp)。
4.在目标数据库中创建一个目录对象。
CREATE DIRECTORY dump_dir AS '/u01/dumps';
5.使用数据泵导入工具将该文件导入到该数据库中。
impdp lora/lora123 DIRECTORY=dump_dir DUMPFILE=ts1_ts2.dmp
如果表空间内的数据量相对较小,则Lora可以只用一条命令执行上面的所有步骤:
impdp lora/lora123 DIRECTORY=dump_dir NETWORK_LINK='srcdb' TABLESPACES=\(ts1,ts2\)
这条命令使用数据泵导入工具将通过数据库链接srcdb(在以前的章节中已讨论过)检索到的数据加载到表中。但是,由于网络带宽通常是受到限制的,因此这种方法可能比使用导出/传输/导入周期方法要慢一些。
如果只需将特定的表或表集合进行转移,那么Lora可以在expdp命令中使用TABLES=<tablelist>子句来只下载特定的表或表集合。
拖出表空间
作为第三种选择,Lora建议使用Oracle数据库10g中的新工具,它简化了可移动表空间的移动方法,因此只涉及执行一个打包过程。在这种方法中,用户利用所提供的DBMS_STREAMS_TABLESPACE_ADM包从源系统中"拖?quot;表空间。这个包使用数据泵转移表空间并将数据文件转换成目标系统的格式。 它还自动执行任何所需的字节顺序变换。
下面给出在最简单的情况下使用这种方法的过程--涉及单个简单表空间(更复杂的情况在下一节介绍)。 如果一个表空间只有一个数据文件,则这个表空间称为简单表空间。Lora演示了DBMS_STREAMS_TABLESPACE_ADM包中PULL_SIMPLE_TABLESPACE过程的使用方法:
1.在存放数据文件的目录所在的(远程)数据仓库数据库中创建一个目录对象。
CREATE DIRECTORY dbf_dir AS '/u01/oradata/dw';
2.设置远程数据库中的表空间TS1为只读。
ALTER TABLESPACE TS1 READ ONLY;
剩下的一些步骤在本地(数据集市)数据库中完成。
3.创建一个连接到远程(数据仓库)数据库(在Lora的例子中是dwdb)的数据库链接。
CREATE DATABASE LINK dwdb USING 'dwdb';
4.创建一个数据文件将被转移到其中的目录对象。
CREATE DIRECTORY dbf_dir AS '/u01/oradata/mart';
5.从远程数据库中拖出表空间。
BEGIN
DBMS_STREAMS_TABLESPACE_ADM
.PULL_SIMPLE_TABLESPACE (
tablespace_name => 'TS1',
database_link => 'dwdb',
directory_object => 'DBF_DIR',
conversion_extension => 'linux'
);
END;
该操作在后台完成了许多步骤:设置源表空间为只读;用数据泵导出工具进行一次表空间的元数据转储;用DBMS_FILE_TRANSFER包移动数据文件和转储的文件;把源表空间恢复到其最初的读写状态;使用数据泵导入工具将表空间插入到本地数据库中。由于源数据库运行在Linux上,而目标数据库运行在Solaris上,因此这一操作首先复制原始数据文件(Linux的文件格式),然后将它转换到目标平台上(Solaris)的文件格式。复制过程保持最初被转移的文件,而创建一个新文件用于转换。新文件与最初的文件同名,但具有CONVERSION_EXTENSION参数指定的linux扩展名。在目标数据库中创建的表空间为只读表空间。
该操作还在与数据文件相同的目录下创建一个名为ts1_01.plg的日志文件。如果执行该过程返回错误信息,则检查该文件的内容可能有助于找到错误的原因。
拖出多个表空间
上面的例子针对的是单个简单表空间的情况。但如果Lora想移动一组表空间,或者一些表空间的数据文件多于一个,该怎么办呢?在这种情况下,她可以使用同一个包中的另一个过程PULL_TABLESPACES。代码清单2给出的例子说明Lora如何转移两个表空间TS7和TS8,而不管它们有多少个数据文件。
该过程要求以VARCHAR2数据类型给出表空间名和目录名。代码清单2中第2行到第5行展示出了这些变量的声明,第10行到第13行展示出这些变量被赋值给相应的表空间名和目录名。 由于定义了两个目录,因此第一个文件在第一个目录中创建,下一个文件在第二个目录中创建,第三个文件再次在第一个目录中创建,如此等等。这些操作通过数据泵作业来执行,作业名在第17行指定。如果需要的话,源系统字节顺序的数据文件会自动转换为目标系统的字节顺序。在目标数据库中创建的新文件获得linux扩展名,如第21行代码所示。处理过程记录在由目录对象LOG_DIR指定的目录中的ts7_ts8.log日志文件中(第14行)。
这种方法的优点显而易见。从一个系统把一个表空间转移到另一个系统所需的所有操作任务都封装在一个程序单元中,并且细节对用户完全透明。甚至把文件从源系统转移到目标系统的工作也在这个过程中通过所提供的DBMS_FILE_TRANSFER包来完成。用户简单地用表空间名调用该过程,表空间就会在本地数据库中被刷新。他们不必操心底层的细节(如操作系统),因为文件转移过程自动转换文件。这种方法有什么不好的地方吗?会上Lora讲到,它的主要缺点就是将各个功能封装在一个单一的过程内,这可能会掩盖某一步产生的错误,使问题的诊断变得很复杂。手工转移一个表空间的方法要求各条命令都是透明的,因而其好处是用户能够看到每一步操作的结果。
会议结束
针对Acme的数据仓库/数据集市体系结构,Lora提出了几种移动数据的可选方法。
第一种可选方法是使用可移动表空间,它能移动完整的表空间集合(不仅包括表,还包括索引、物化视图和其他对象)。通常它还是这三种方法中最快的一种。但是,它的一个主要缺点是对指定的表空间必须在复制文件时设置为只读。
第二种方法是使用数据泵,它对表空间是否为只读没有要求。当只需要移动指定的表而不是整个表空间时,这种方法很有用。
最后一种方法是拖出表空间,该方法把可移动表空间方法的所有步骤组合成一步操作。用这种方法复制数据非常简单,但要想调整每个具体步骤以便进行性能优化时,它为数据库管理员提供的灵活性太少。
在会议结束时,高级管理层对Lora表示感谢,而Lora也感谢高级管理层对Acme银行迁移到Oracle数据库10g的支持,因为这使得不同的数据移动方法都成为可行的。
权限系统概要
转贴:http://www.cnblogs.com/xspin/articles/31395.html
前言:
权限往往是一个极其复杂的问题,但也可简单表述为这样的逻辑表达式:判断"Who对What(Which)进行How的操作"的逻辑表达式是否为真。针对不同的应用,需要根据项目的实际情况和具体架构,在维护性、灵活性、完整性等N多个方案之间比较权衡,选择符合的方案。
对Swing线程的再思索 (上)
不正确的Swing线程是运行缓慢、无响应和不稳定的Swing应用的主要原因之一。这是许多原因造成的,从开发人员对Swing单线程模型的误解,到保证正确的线程执行的困难。即使对Swing线程进行了很多努力,应用线程逻辑也是很难理解和维护的。本文阐述了如何在开发Swing应用中使用事件驱动编程,以大大简化开发、维护,并提供高灵活性。
背景
既然我们是要简化Swing应用的线程,首先让我们来看看Swing线程是怎么工作的,为什么它是必须的。Swing API是围绕单线程模型设计的。这意味着Swing组件必须总是通过同一个线程来修改和操纵。为什么采用单线程模型,这有很多原因,包括开发成本和同步Swing的复杂性--这都会造成一个迟钝的API。为了达到单线程模型,有一个专门的线程用于和Swing组件交互。这个线程就是大家熟知的Swing线程,AWT(有时也发音为"ought")线程,或者事件分派线程。在本文的下面的部分,我选用Swing线程的叫法。
既然Swing线程是和Swing组件进行交互的唯一的线程,它就被赋予了很多责任。所有的绘制和图形,鼠标事件,组件事件,按钮事件,和所有其它事件都发生在Swing线程。因为Swing线程的工作已经非常沉重了,当太多其它工作在Swing线程中进行处理时就会发生问题。会引起这个问题的最常见的位置是在非Swing处理的地方,像发生在一个事件监听器方法中,比如JButton的ActionListener,的数据库查找。既然ActionListener的actionPerformed()方法自动在Swing线程中执行,那么,数据库查找也将在Swing线程中执行。这将占用了Swing的工作,阻止它处理它的其它任务--像绘制,响应鼠标移动,处理按钮事件,和应用的缩放。用户以为应用死掉了,但实际上并不是这样。在适当的线程中执行代码对确保系统正常地执行非常重要。
既然我们已经看到了在适当的线程中执行Swing应用的代码是多么重要,现在让我们如何实现这些线程。我们看看将代码放入和移出Swing线程的标准机制。在讲述过程中,我将突出几个和标准机制有关的问题和难点。正如我们看到的,大部分的问题都来自于企图在异步的Swing线程模型上实现同步的代码模型。从那儿,我们将看到如何修改我们的例子到事件驱动--移植整个方式到异步模型。
通用Swing线程解决方案
让我们以一个最常用的Swing线程错误开始。我们将企图使用标准的技术来修正这个问题。在这个过程中,我们将看到实现正确的Swing线程的复杂性和常见困难。并且,注意在修正这个Swing线程问题中,许多中间的例子也是不能工作的。在例子中,我在代码失败的地方以//broken开头标出。好了,现在,让我们进入我们的例子吧。
假设我们在执行图书查找。我们有一个简单的用户界面,包括一个查找文本域,一个查找按钮,和一个输出的文本区域。这个接口如图1所示。不要批评我的UI设计,这个确实很丑陋,我承认。
图 1. 基本查询用户界面
用户输入书的标题,作者或者其它条件,然后显示一个结果的列表。下面的代码例子演示了按钮的ActionListener在同一个线程中调用lookup()方法。在这些例子中,我使用了thread.sleep()休眠5秒来作为一个占位的外部查找。线程休眠的结果等同于一个耗时5秒的同步的服务器调用。
private void searchButton_actionPerformed() {
outputTA.setText("Searching for: " +
searchTF.getText());
//Broken!! Too much work in the Swing thread
String[] results = lookup(searchTF.getText());
outputTA.setText("");
for (int i = 0; i < results.length; i++) {
String result = results[i];
outputTA.setText(outputTA.getText() +
'\n' + result);
}
}
如果你运行这段代码(完整的代码可以在这儿下载),你会立即发现存在一些问题。图2显示了查找运行中的一个屏幕截图。
图 2. 在Swing线程中进行查找
注意Go按钮看起来是被按下了。这是因为actionPerformed方法通知了按钮绘制为非按下外观,但是还没有返回。你也会发现要查找的字串"abcde"并没有出现在文本区域中。searchButton_actionPerformed的第1行代码将文本区域设置为要查找的字串。但是,注意Swing重画并不是立即执行的。而是把重画请求放置到Swing事件队列中等待Swing线程处理。但是这儿,我们因查找处理占用了Swing线程,所以,它还不能马上进行重画。
要修正这些问题,让我们把查找操作移入非Swing线程中。我们第一个想到的就是让整个方法在一个新的线程中执行。这样作的问题是Swing组件,本例中的文本区域,只能从Swing线程中进行编辑。下面是修改后的searchButton_actionPerformed方法:
private void searchButton_actionPerformed() {
outputTA.setText("Searching for: " +
searchTF.getText());
//the String[][] is used to allow access to
// setting the results from an inner class
final String[][] results = new String[1][1];
new Thread(){
public void run() {
results[0] = lookup(searchTF.getText());
}
}.start();
outputTA.setText("");
for (int i = 0; i < results[0].length; i++) {
String result = results[0][i];
outputTA.setText(outputTA.getText() +
'\n' + result);
}
}
这种方法有很多问题。注意final String[][] 。这是一个处理匿名内部类和作用域的不得已的替代。基本上,在匿名内部类中使用的,但在外部环绕类作用域中定义的任何变量都需要定义为final。你可以通过创建一个数组来持有变量解决这个问题。这样的话,你可以创建数组为final的,修改数组中的元素,而不是数组的引用自身。既然我们已经解决这个问题,让我们进入真正的问题所在吧。图3显示了这段代码运行时发生的情况:

图 3. 在Swing线程外部进行查找
界面显示了一个null,因为显示代码在查找代码完成前被处理了。这是因为一旦新的线程启动了,代码块继续执行,而不是等待线程执行完毕。这是那些奇怪的并发代码块中的一个,下面将把它编写到一个方法中使其能够真正执行。
在SwingUtilities类中有两个方法可以帮助我们解决这些问题:invokerLater()和invokeAndWait()。每一个方法都以一个Runnable作为参数,并在Swing线程中执行它。invokeAndWait()方法阻塞直到Runnnable执行完毕;invokeLater()异步地执行Runnable。invokeAndWait()一般不赞成使用,因为它可能导致严重的线程死锁,对你的应用造成严重的破坏。所以,让我们把它放置一边,使用invokeLater()方法。
要修正最后一个变量变量scooping和执行顺序的问题,我们必须将文本区域的getText()和setText()方法调用移入一个Runnable,只有在查询结果返回后再执行它,并且在Swing线程中执行。我们可以这样作,创建一个匿名Runnable传递给invokeLater(),包括在新线程的Runnable后的文本区域操作。这保证了Swing代码不会在查找结束之前执行。下面是修正后的代码:
private void searchButton_actionPerformed() {
outputTA.setText("Searching for: " +
searchTF.getText());
final String[][] results = new String[1][1];
new Thread() {
public void run() {
//get results.
results[0] = lookup(searchTF.getText());
// send runnable to the Swing thread
// the runnable is queued after the
// results are returned
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
// Now we're in the Swing thread
outputTA.setText("");
for (int i = 0;
i < results[0].length;
i++) {
String result = results[0][i];
outputTA.setText(
outputTA.getText() +
'\n' + result);
}
}
}
);
}
}.start();
}
这可以工作,但是这样做令人非常头痛。我们不得不对通过匿名线程执行的顺序,我们还不得不处理困难的scooping问题。问题并不少见,并且,这只是一个非常简单的例子,我们已经遇到了作用域,变量传递,和执行顺序等一系列问题。相像一个更复杂的问题,包含了几层嵌套,共享的引用和指定的执行顺序。这种方法很快就失控了。
问题
我们在企图强制通过异步模型进行同步执行--企图将一个方形的螺栓放到一个圆形的空中。只有我们尝试这样做,我们就会不断地遭遇这些问题。从我的经验,可以告诉你这些代码很难阅读,很难维护,并且易于出错。
这看起来是一个常见的问题,所以一定有标准的方式来解决,对吗?出现了一些框架用于管理Swing的复杂性,所以让我们来快速预览一下它们可以做什么。
一个可以得到的解决方案是Foxtrot,一个由Biorn Steedom写的框架,可以在SourceForge上获取。它使用一个叫做Worker的对象来控制非Swing任务在非Swing线程中的执行,阻塞直到非Swing任务执行完毕。它简化了Swing线程,允许你编写同步代码,并在Swing线程和非Swing线程直接切换。下面是来自它的站点的一个例子:
public void actionPerformed(ActionEvent e)
{
button.setText("Sleeping...");
String text = null;
try
{
text = (String)Worker.post(new Task()
{
public Object run() throws Exception
{
Thread.sleep(10000);
return "Slept !";
}
});
}
catch (Exception x) ...
button.setText(text);
somethingElse();
}
注意它是如何解决上面的那些问题的。我们能够非常容易地在Swing线程中传入传出变量。并且,代码块看起来也很正确--先编写的先执行。但是仍然有一些问题障碍阻止使用从准同步异步解决方案。Foxtrot中的一个问题是异常管理。使用Foxtrot,每次调用Worker必须捕获Exception。这是将执行代理给Worker来解决同步对异步问题的一个产物。
同样以非常相似的方式,我此前也创建了一个框架,我称它为链接运行引擎(Chained Runnable Engine) ,同样也遭受来自类似同步对异步问题的困扰。使用这个框架,你将创建一个将被引擎执行的Runnable的集合。每一个Runnable都有一个指示器告诉引擎是否应该在Swing线程或者另外的线程中执行。引擎也保证Runnable以正确的顺序执行。所以Runnable #2将不会放入队列直到Runnable #1执行完毕。并且,它支持变量以HashMap的形式从Runnable到Runnable传递。
表面上,它看起来解决了我们的主要问题。但是当你深入进去后,同样的问题又冒出来了。本质上,我们并没有改变上面描述的任何东西--我们只是将复杂性隐藏在引擎的后面。因为指数级增长的Runnable而使代码编写将变得非常枯燥,也很复杂,并且这些Runnable常常相互耦合。Runnable之间的非类型的HashMap变量传递变得难于管理。问题的列表还有很多。
在编写这个框架之后,我意识到这需要一个完全不同的解决方案。这让我重新审视了问题,看别人是怎么解决类似的问题的,并深入的研究了Swing的源代码。
对Swing线程的再思索 (下)
原文:http://today.java.net/pub/a/today/2003/10/24/swing.html?page=2
http://blog.csdn.net/chensheng913/archive/2004/08/29/88360.aspx
解决方案:事件驱动编程
所有前面的这些解决方案都存在一个共同的致命缺陷--企图在持续地改变线程的同时表示一个任务的功能集。但是改变线程需要异步的模型,而线程异步地处理Runnable。问题的部分原因是我们在企图在一个异步的线程模型之上实现一个同步的模型。这是所有Runnable之间的链和依赖,执行顺序和内部类scooping问题的根源。如果我们可以构建真正的异步,我们就可以解决我们的问题并极大地简化Swing线程。
在这之前,让我们先列举一下我们要解决的问题:
1. 在适当的线程中执行代码
2. 使用SwingUtilities.invokeLater()异步地执行.
异步地执行导致了下面的问题:
1. 互相耦合的组件
2. 变量传递的困难
3. 执行的顺序
让我们考虑一下像Java消息服务(JMS)这样的基于消息的系统,因为它们提供了在异步环境中功能组件之间的松散耦合。消息系统触发异步事件,正如在Enterprise Integration Patterns 中描述的。感兴趣的参与者监听该事件,并对事件做成响应--通常通过执行它们自己的一些代码。结果是一组模块化的,松散耦合的组件,组件可以添加到或者从系统中去除而不影响到其它组件。更重要的,组件之间的依赖被最小化了,而每一个组件都是良好定义的和封装的--每一个都仅对自己的工作负责。它们简单地触发消息,其它一些组件将响应这个消息,并对其它组件触发的消息进行响应。
现在,我们先忽略线程问题,将组件解耦并移植到异步环境中。在我们解决了异步问题后,我们将回过头来看看线程问题。正如我们所将要看到的,那时解决这个问题将非常容易。
让我们还拿前面引入的例子,并把它移植到基于事件的模型。首先,我们把lookup调用抽象到一个叫LookupManager的类中。这将允许我们将所有UI类中的数据库逻辑移出,并最终允许我们完全将这两者脱耦。下面是LookupManager类的代码:
class LookupManager {
private String[] lookup(String text) {
String[] results = ...
// database lookup code
return results
}
}
现在我们开始向异步模型转换。为了使这个调用异步化,我们需要抽象调用的返回。换句话,方法不能返回任何值。我们将以分辨什么相关的动作是其它类所希望知道的开始。在我们这个例子中最明显的事件是搜索结束事件。所以让我们创建一个监听器接口来响应这些事件。该接口含有单个方法lookupCompleted()。下面是接口的定义:
interface LookupListener {
public void lookupCompleted(Iterator results);
}
遵守Java的标准,我们创建另外一个称作LookupEvent的类包含结果字串数组,而不是到处直接传递字串数组。这将允许我们在不改变LookupListener接口的情况下传递其它信息。例如,我们可以在LookupEvent中同时包括查找的字串和结果。下面是LookupEvent类:
public class LookupEvent {
String searchText;
String[] results;
public LookupEvent(String searchText) {
this.searchText = searchText;
}
public LookupEvent(String searchText,
String[] results) {
this.searchText = searchText;
this.results = results;
}
public String getSearchText() {
return searchText;
}
public String[] getResults() {
return results;
}
}
注意LookupEvent类是不可变的。这是很重要的,因为我们并不知道在传递过程中谁将处理这些事件。除非我们创建事件的保护拷贝来传递给每一个监听者,我们需要把事件做成不可变的。如果不这样,一个监听者可能会无意或者恶意地修订事件对象,并破坏系统。
现在我们需要在LookupManager上调用lookupComplete()事件。我们首先要在LookupManager上添加一个LookupListener的集合:
List listeners = new ArrayList();
并提供在LookupManager上添加和去除LookupListener的方法:
public void addLookupListener(LookupListener listener){
listeners.add(listener);
}
public void removeLookupListener(LookupListener listener){
listeners.remove(listener);
}
当动作发生时,我们需要调用监听者的代码。在我们的例子中,我们将在查找返回时触发一个lookupCompleted()事件。这意味着在监听者集合上迭代,并使用一个LookupEvent事件对象调用它们的lookupCompleted()方法。
我喜欢把这些代码析取到一个独立的方法fire[event-method-name] ,其中构造一个事件对象,在监听器集合上迭代,并调用每一个监听器上的适当的方法。这有助于隔离主要逻辑代码和调用监听器的代码。下面是我们的fireLookupCompleted方法:
private void fireLookupCompleted(String searchText,
String[] results){
LookupEvent event =
new LookupEvent(searchText, results);
Iterator iter =
new ArrayList(listeners).iterator();
while (iter.hasNext()) {
LookupListener listener =
(LookupListener) iter.next();
listener.lookupCompleted(event);
}
}
第2行代码创建了一个新的集合,传入原监听器集合。这在监听器响应事件后决定在LookupManager中去除自己时将发挥作用。如果我们不是安全地拷贝集合,在一些监听器应该 被调用而没有被调用时发生令人厌烦的错误。
下面,我们将在动作完成时调用fireLookupCompleted辅助方法。这是lookup方法的返回查询结果的结束处。所以我们可以改变lookup方法使其触发一个事件而不是返回字串数组本身。下面是新的lookup方法:
public void lookup(String text) {
//mimic the server call delay...
try {
Thread.sleep(5000);
} catch (Exception e){
e.printStackTrace();
}
//imagine we got this from a server
String[] results =
new String[]{"Book one",
"Book two",
"Book three"};
fireLookupCompleted(text, results);
}
现在让我们把监听器添加到LookupManager。我们希望当查找返回时更新文本区域。以前,我们只是直接调用setText()方法。因为文本区域是和数据库调用一起都在UI中执行的。既然我们已经将查找逻辑从UI中抽象出来了,我们将把UI类作为一个到LookupManager的监听器,监听lookup事件并相应地更新自己。首先我们将在类定义中实现监听器接口:
public class FixedFrame implements LookupListener
接着我们实现接口方法:
public void lookupCompleted(final LookupEvent e) {
outputTA.setText("");
String[] results = e.getResults();
for (int i = 0; i < results.length; i++) {
String result = results[i];
outputTA.setText(outputTA.getText() +
"\n" + result);
}
}
最后,我们将它注册为LookupManager的一个监听器:
public FixedFrame() {
lookupManager = new LookupManager();
//here we register the listener
lookupManager.addListener(this);
initComponents();
layoutComponents();
}
为了简化,我在类的构造器中将它添加为监听器。这在大多数系统上都允许良好。当系统变得更加复杂时,你可能会重构、从构造器中提炼出监听器注册代码,以允许更大的灵活性和扩展性。
到现在为止,你看到了所有组件之间的连接,注意职责的分离。用户界面类负责信息的显示--并且仅负责信息的显示。另一方面,LookupManager类负责所有的lookup连接和逻辑。并且,LookupManager负责在它变化时通知监听器--而不是当变化发生时应该具体做什么。这允许你连接任意多的监听器。
为了演示如何添加新的事件,让我们回头添加一个lookup开始的事件。我们可以添加一个称作lookupStarted()的事件到LookupListener,我们将在查找开始执行前触发它。我们也创建一个fireLookupStarted()事件调用所有LookupListener的lookupStarted()。现在lookup方法如下:
public void lookup(String text) {
fireLookupStarted(text);
//mimic the server call delay...
try {
Thread.sleep(5000);
} catch (Exception e){
e.printStackTrace();
}
//imagine we got this from a server
String[] results =
new String[]{"Book one",
"Book two",
"Book three"};
fireLookupCompleted(text, results);
}
我们也添加新的触发方法fireLookupStarted()。这个方法等同于fireLookupCompleted()方法,除了我们调用监听器上的lookupStarted()方法,并且该事件也不包含结果集。下面是代码:
private void fireLookupStarted(String searchText){
LookupEvent event =
new LookupEvent(searchText);
Iterator iter =
new ArrayList(listeners).iterator();
while (iter.hasNext()) {
LookupListener listener =
(LookupListener) iter.next();
listener.lookupStarted(event);
}
}
最后,我们在UI类上实现lookupStarted()方法,设置文本区域提示当前搜索的字符串。
public void lookupStarted(final LookupEvent e) {
outputTA.setText("Searching for: " +
e.getSearchText());
}
这个例子展示了添加新的事件是多么容易。现在,让我们看看展示事件驱动脱耦的灵活性。我们将通过创建一个日志类,当一个搜索开始和结束时在命令行中输出信息来演示。我们称这个类为Logger。下面是它的代码:
public class Logger implements LookupListener {
public void lookupStarted(LookupEvent e) {
System.out.println("Lookup started: " +
e.getSearchText());
}
public void lookupCompleted(LookupEvent e) {
System.out.println("Lookup completed: " +
e.getSearchText() +
" " +
e.getResults());
}
}
现在,我们添加Logger作为在FixedFrame构造方法中的LookupManager的一个监听器。
public FixedFrame() {
lookupManager = new LookupManager();
lookupManager.addListener(this);
lookupManager.addListener(new Logger());
initComponents();
layoutComponents();
}
现在你已经看到了添加新的事件、创建新的监听器--向您展示了事件驱动方案的灵活性和扩展性。你会发现随着你更多地开发事件集中的程序,你会更加娴熟地在你的应用中创建通用动作。像其它所有事情一样,这只需要时间和经验。看起来在事件模型上已经做了很多研究,但是你还是需要把它和其它替代方案相比较。考虑开发时间成本;最重要的,这是一次性成本。一旦你创建好了监听器模型和它们的动作,以后向你的应用中添加监听器将是小菜一蝶。
线程
到现在,我们已经解决了上面的异步问题;通过监听器使组件脱耦,通过事件对象传递变量,通过事件产生和监听器的注册的组合决定执行的顺序。让我们回到线程问题,因为正是它把我们带到了这儿。实际上非常容易:因为我们已经有了异步功能的监听器,我们可以简单地让监听器自己决定它们应该在哪个线程中执行。考虑UI类和LookupManager的分离。UI类基于事件,决定需要什么处理。并且,该类也是Swing,而日志类不是。所以让UI类负责决定它应该在什么线程中执行将更加有意义。所以,让我们再次看看UI类。下面是没有线程的lookupCompleted()方法:
public void lookupCompleted(final LookupEvent e) {
outputTA.setText("");
String[] results = e.getResults();
for (int i = 0; i < results.length; i++) {
String result = results[i];
outputTA.setText(outputTA.getText() +
"\n" + result);
}
}
我们知道这将在非Swing线程中调用,因为该事件是直接在LookupManager中触发的,这将不是在Swing线程中执行。因为所有的代码功能上都是异步的(我们不必等待监听器方法允许结束后才调用其它代码),我们可以通过SwingUtilities.invokeLater()将这些代码改道到Swing线程。下面是新的方法,传入一个匿名Runnable到SwingUtilities.invokeLater():
public void lookupCompleted(final LookupEvent e) {
//notice the threading
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
outputTA.setText("");
String[] results = e.getResults();
for (int i = 0;
i < results.length;
i++) {
String result = results[i];
outputTA.setText(outputTA.getText() +
"\n" + result);
}
}
}
);
}
如果任何LookupListener不是在Swing线程中执行,我们可以在调用线程中执行监听器代码。作为一个原则,我们希望所有的监听器都迅速地接到通知。所以,如果你有一个监听器需要很多时间来处理自己的功能,你应该创建一个新的线程或者把耗时代码放入ThreadPool中等待执行。
最后的步骤是让LookupManager在非Swing线程中执行lookup。当前,LookupManager是在JButton的ActionListener的Swing线程中被调用的。现在是我们做出决定的时候,或者我们在JButton的ActionListener中引入一个新的线程,或者我们可以保证lookup自己在非Swing线程中执行,自己开始一个新的线程。我选择尽可能和Swing类贴近地管理Swing线程。这有助于把所有Swing逻辑封装在一起。如果我们把Swing线程逻辑添加到LookupManager,我们将引入了一层不必要的依赖。并且,对于LookupManager在非Swing线程环境中孵化自己的线程是完全没有必要的,比如一个非绘图的用户界面,在我们的例子中,就是Logger。产生不必要的新线程将损害到你应用的性能,而不是提高性能。LookupManager执行的很好,不管Swing线程与否--所以,我喜欢把代码集中在那儿。
现在我们需要将JButton的ActionListener执行lookup的代码放在一个非Swing线程中。我们创建一个匿名的Thread,使用一个匿名的Runnable执行这个lookup。
private void searchButton_actionPerformed() {
new Thread(){
public void run() {
lookupManager.lookup(searchTF.getText());
}
}.start();
}
这就完成了我们的Swing线程。简单地在actionPerformed()方法中添加线程,确保监听器在新的线程中执行照顾到了整个线程问题。注意,我们不用处理像第一个例子那样的任何问题。通过把时间花费在定义一个事件驱动的体系,我们在和Swing线程相关处理上节约了更多的时间。
结论
如果你需要在同一个方法中执行大量的Swing代码和非Swing代码,很容易将某些代码放错位置。事件驱动的方式将迫使你将代码放在它应该在的地方--它仅应该在的地方。如果你在同一个方法中执行数据库调用和更新UI组件,那么你就在一个类中写入了太多的逻辑。分析你系统中的事件,创建底层的事件模型将迫使你将代码放到正确的地方。将费时的数据库调用代码放在非UI类中,也不要在非UI组件中更新UI组件。采用事件驱动的体系,UI负责UI更新,数据库管理类负责数据库调用。在这一点上,每一个封装的类都只用关心自己的线程,不用担心系统其它部分如何动作。当然,设计、构建一个事件驱动的客户端也很有用,但是需要花费的时间代价远超过带来的结果系统的灵活性和可维护性的提高。
Swing线程的最后讨论 -- 利用异步模型
转贴自:
http://www.softhouse.com.cn/html/200410/2004102610471400001367.html
在钻研树、表和异步模型之前,我首先回顾一下Swing的单线程规则(single-thread rule)并检验它的含义。
Swing的单线程规则是说,Swing组件在某一时刻仅能被一个线程访问。这个规则对gets和sets都有效,并且"一个线程"通常指的都是事件派发线程。
单线程规则应用于UI组件是非常相称的,因为UI组件往往都以单线程方式使用,并由用户发起大多数动作。构建线程安全的组件是困难而冗长乏味的:如果能避免则最好不过。但除了它的好处,单线程规则有更多的含义。
Swing组件所有的事件都在事件派发线程(event-dispatch thread)中发送和接收,除此之外才不遵守单线程规则。例如,属性变更事件(property-change events)应该在事件派发线程中发送,模型变更事件(model-change events)应该在事件派发线程中接收。
对于基于模型的组件如JTable和JTree来说,单线程规则意味着仅能在事件派发线程中访问模型本身。出于这个理由,模型中的方法必须执行得很快并且决不能阻塞,否则整个用户界面会响应迟钝。
但想象一下你有一个带TreeModel的JTree要访问一个远程服务器?你会发现当服务器不可用或过于繁忙时,你对的TreeModel的调用会阻塞,并使得整个用户界面被冻结。你能做些什么来改进界面的响应能力?
再假设你有一个带TableModel的JTable管理着网络上的一个设备?你会发现当被管理的设备繁忙或网络拥挤时,界面变得迟钝。你该怎么办?
这些困难的情况在召唤线程。当需求浮现,我们还是有几种途径在Swing中使用线程,尽管存在单线程规则。
本文展现了两种方法来使用线程访问缓慢的、远程的或其他异步的模型,并显示了如何在JTree和JTable组件中使用。(不仅仅由事件派发线程访问的模型被称为"异步"模型。)
-----------------------------------------------------------------------------
假设你有一个带TreeModel的JTree要访问远程服务器,但服务器又是缓慢或不可靠的,你该怎么办?
DynamicTree演示了如何使用后台线程动态展开JTree的节点。
图1是正在处理节点展开的DynamicTree。
图1
DynamicTree基于一种分裂模型设计(split-model design)。在这个设计中,真正的模型是一个异步模型,它可能是缓慢或不可靠的。JTree模型使用一个标准的同步模型来维持一个真正的模型的snapshot。(异步模型可能位于远程服务器,出于这个原因我通常称之为远程模型,并且我把Swing组件的模型称为本地模型。)
使用本地模型来影射或缓存远程模型有助于提供一个在所有时刻都可靠的、高响应能力的Swing组件。但这种方法的一个缺点,也是必要的代价是,模型数据不得不重复。另一个问题是,两个模型并非总是同步的,而且它们之间不得不用某种方法来互相协调。
模型间的一种协调方法是仅在需要数据时更新本地模型,而非在此之前。这种技术在远程模型很慢或很大时是有用的,但这种技术要求模型基本上是静态的。DynamicTree用这种方法来浏览一个缓慢的静态树模型。
DynamicTree开始时是一个未展开的根节点,同时有一个展开监听器(expansion listener)等待用户输入来展开节点。当节点展开后,展开监听器启动一个SwingWorker线程。这个worker的construct()方法访问远程模型并返回新的子节点。然后worker的finished()方法会在事件派发线程中执行,将子节点添加到本地模型。
为简单起见,在同一时刻仅允许一个worker运行。如果有任何节点被折叠,当前活动的任何worker将被中断。(程序并不检查折叠的节点是否是worker正在展开的节点的祖先。)

图2 DynamicTree执行顺序图
本节的余下部分将讨论类结构和实现。你可以也跳过下面的远程模型演示程序。后面的"下载"一节解释了如何下载和运行该演示程序。
图3是一个概略的类结构图。

图3 DynamicTree各类
本地模型是一个包含DefaultMutableTreeNode节点的DefaultTreeModel。节点必须是可变的,以便能够动态地添加子节点。可变节点的userObject域可以用来指向远程模型。
远程模型实现了TreeNodeFactory接口:
public interface TreeNodeFactory {
DefaultMutableTreeNode[] createChildren(Object userObject)
throws Exception;
}
createChildren()在worker线程中被调用。类似于大多数异步和远程方法,它会抛出一个异常(一般是一个RemoteException或InterruptedException)。userObject参数是最近展开的节点指回远程模型的一个链接。一次返回一个包括所有子节点的数组比单独返回每个子节点更高效,还可以避免面对部分失败的情况。
在初始化时,每个子节点都要被设置allowsChildren属性和一个指回远程模型的链接。(如果远程节点是叶子节点,则allowsChildren属性设为false;否则allowsChildren被设为true以表明该节点可被展开。)
DefaultTreeNodeFactory是一个Adapter(请参考GOF的《Design Patterns》)。它将任意TreeModel配接成TreeNodeFactory接口。演示程序中用的是缺省树模型。在main方法中的一些注释掉的代码演示了如何安装一个FileSystemNodeFactory。SlowTreeNodeFactory是演示用的一个包装类;它在运行中插入随机的延迟。
我已经努力使DynamicTree保持简单。节点中除了节点的名字别无其他内容。在节点中需要填充内容的情况下,如果能异步地加载内容会好一些,比如你可能用一个树选择监听器(tree selection listener)来初始化加载过程。
下一节的远程表演示程序会更实用一些。
-------------------------------------------------------------------------------
假设你有一个带TableModel的JTable管理着一个远程设备,但接口在设备繁忙时慢如龟爬,你该怎么办?
下面的远程表演示程序使用后台线程和异步的回调操作(asynchronous callbacks)来显示或修改一个远程表模型。
图4是一个远程表编辑器正在向服务器发送新的值。未处理的单元格在更新完成前会保持黄色。
图4
远程表演示程序用RMI实现,它由一个服务器和两个客户端(一个编辑器和一个查看器)组成。查看器实际上只是一个禁止了编辑功能的编辑器。
客户端使用一个RemoteTable组件bean,RemoteTable是设计成于远程表模型协作的JTable的子类。图4所示的编辑器由一个RemoteTable组件、一个状态标签、一个简单的活动计量器(右下角)和一些用于定位服务器的代码组成。
DynamicTree仅在需要数据时更新它的本地模型,与此不同的是,RemoteTable组件在一开始就获取所有的数据,然后监听此后的变化。基于通知(notification-based)的技术可以和动态模型一起工作,且在模型比较小时工作的很好。
对单元格的修改驱动通知。当用户完成对单元格的编辑,RemoteTable把编辑过的单元格标记为未处理的(黄色突出显示)并且安排一项SwingWorker任务。该woker的construct()方法会把新的值发送到远程模型。
当远程模型接收到新的值,它把变化通知给监听器。Worker的finished()方法的唯一功能是确认任务已完成;在来自远程模型的通知接受并处理完后,黄色的未处理单元格变回普通单元格。
RemoteTable用一个QueuedExecutor调度它的SwingWorker任务,QueuedExecutor在单个线程中顺序执行所有的任务。(QueuedExecutor是Doug Lea的util.concurrent包的一部分,请参见后面的"参考资料"部分。)远程模型用RMI回调操作通知它的监听器。
为了支持可视的反馈,RemoteTable发送Task事件给注册的Task监听器。在任务进入调度时,监听器的taskStarted()被调用,taskEnded()在任务完成时被调用。客户端演示程序使用这些事件来启动或停止一个小动画并更新状态来。
图5表示的是单元格的更新过程。执行过程的开始和结束都位于左边的事件派发线程。SwingWorker任务则在右边的executor的线程中执行。Worker的finished()方法的执行过程没有表示出来。
图5 RemoteTable执行顺序图
简单起见,远程模型并没有对互相冲突的编辑提供保护。所以在同一时刻仅能有一个编辑器在运行。(并发编辑可以用添加请求ID(request IDs)的方法来实现。)
所作的另一项简化决定是客户端和服务器必须预先对表的列结构协商好。换句话说,服务器性客户端提供行数据,而客户端必须已经知道他们要处理的是什么表。演示用的客户端用DefaultModelTemplate来预先定义了各列的名称和类,来确定哪个单元格可以被编辑。(在演示程序中,前两列是不可编辑的。)
本节的余下部分阐述了类结构和实现。如果不想了解这个演示程序中所用的修订版的SwingWorker,你可以跳过去。"下载"一节解释了如何下载并运行这个演示程序。
图6是一个概略的类结构图。
图6 RemoteTable各类
远程模型实现了RemoteTableModel接口,这个接口和AbstractTableModel很相似,除了它的所有方法都会抛出异常。要启动一个客户端,远程表模型发送一个完全更新事件给客户端已注册的的监听器。
RemoteTableModelAdapter配接任意的TableModel到一个RemoteTableModel。演示程序中的表模型取自The Java Tutorial,但插入了一些延迟来模拟实际情况。远程表模型事件包含已更新的单元格的值。
RemoteTable组件用一个DefaultRemoteTableModelListener来接受来自远程模型的回调。这个监听器会在事件派发线程中更新本地模型。因为远程模型可能要通知插入或删除某些行,所以监听器要求本地模型支持插入和删除操作,DefaultTableModel满足这个要求。
-------------------------------------------------------------------------------
演示程序用SwingWorker在后台执行费时的操作,然后更新UI。
这个演示程序所用的SwingWorker是基于《使用SwingWorker线程》文中提出的SwingWorker类,但重新实现了它以修正一处竞态条件,添加超时支持,和改进了异常处理。
这个新的实现还基于Doug Lea的util.concurrent包的FutureResult类(参见"参考资料"一节)。由于大量依赖了FutureResult所做的工作,SwingWorker类的实现是简单而灵活的。
本节的余下部分更详细地描述了实现的细节,请继续往下看或直接跳到后面下载源码。
FutureResult,正如它的名字所暗示的,它是用来保持某动作的结果的。它被设计成和一个Callable共同使用,Callable是一个会返回结果的runnable动作:
public interface Callable {
Object call() throws Exception;
}
新的SwingWorker是一个Runnable FutureResult。在运行时,它把结果设成construct()的返回值,然后在事件派发线程中调用finished()方法。(注意:SwingWorker是一个抽象类;你要子类化它并实现construct()和finished()。)
下面的代码来自SwingWorker的run()方法:
Callable function = new Callable() {
public Object call() throws Exception {
return construct();
}
};
Runnable doFinished = new Runnable() {
public void run() {
finished();
}
};
setter(function).run();
SwingUtilities.invokeLater(doFinished);
第一段把construct()转换成一个Callable动作,第二段把finished()转换成作为Runnable的doFinished。然后setter(function)被运行,doFinished被调用。
上面缺少的部分是setter(function)。它创建一个刻板的Runnable。在运行时,这个Runnable调用参数指定的function,然后给结果设置返回值。下面是来自FutureResult的代码:
public Runnable setter(final Callable function) {
return new Runnable() {
public void run() {
try {
set(function.call());
}
catch(Throwable ex) {
setException(ex);
}
}
};
}
注意try-catch块所作的防护。如果construct()抛出任何东西(Exception、Error等等),都会被捕捉并记录下来。
调用start()来启动worker线程。这是修订版的SwingWorker和原来版本的一个重要区别。
在原来的版本中,SwingWorker()构造器自动启动线程,这种做法带来了一个线程和子类构造器竞争的危险:当SwingWorker()构造器已启动了线程,而子类的构造器还没完成。弥补方法是,先构造SwingWorker,然后再调用start()。
顺便一提,RemoteTable并不调用start()。正确来说,SwingWorker是作为一个Runnable被QueuedExecutor执行的。
新的SwingWorker支持超时,这是通过覆盖getTimeout()方法已返回一个非零值来实现的。当超出超时时间,worker线程会被中断。
如果想查看使用超时的例子,请参阅注释版的getTimeout()方法和DynamicTree如何处理TimeoutException。
超时功能是用TimedCallable来实现的,其中使用了FutureResult的timedGet()方法。
construct()方法抛出的任何东西都会被记录。除了死循环和死锁,新的异常处理确保了SwingWorker处于"准备好"的状态。也就是说,它要么得到一个正确的结果,要么得到一个异常。
下面的get()方法用来取出结果。这个方法继承自FutureResult:
public Object get()
throws InvocationTargetException, InterruptedException
如果construct()抛出一个Exception,get()方法就会抛出InvocationTargetException。要获得construct()方法实际上抛出的异常,可以调用getTargetException()。
如果取结果的线程在等待结果的过程中被中断,get()方法会抛出InterruptedException——但这种情况对SwingWorker来说很罕见,因为取结果的线程通常都是事件派发线程,并且在finished()会被调用以前,结果总是已经准备好的。
SwingWorker的实现在jozart.swingutils包中。在同一个包里,你还能找到InvokeUtils类,这个类还提供了几个invokeXXX()方法。后台线程可以用这些方法来在事件派发线程中获取值和用户输入,再把结果返回到后台线程。
-------------------------------------------------------------------------------
所有演示程序的源码,还有编译好的类文件和资源,都包括在这个zip文件中:threads3 demos.zip
threads3_demos.zip文件包括以下内容:
• jozart/
o dynamictree/ - DynamicTree 演示程序。
o remotetable/ - RemoteTable bean 源码。
o remotetabledemo/ - RemoteTable演示程序。
o swingutils/ - SwingWorker.
• EDU/ - util.concurrent 包(只包括了用到的类)。
• java.policy - RMI 安全策略文件(security policy)。
• remotetable.jar - RemoteTable bean (供各IDE使用)
注意:来自util.concurrent的类是从v1.2.3版选出的。只包括了演示程序中用到的类。
注意:运行这些演示程序需要Java 2。(util.concurrent包有几处用到了Collections。)
运行演示程序,首先请解压threads3_demos.zip到一个空文件夹,注意不要改变里面文件夹的名字。然后换到你把文件解压到的那个文件夹。
将DynamicTree作为applet运行:
> appletviewer jozart/dynamictree/DynamicTree.html
将DynamicTree作为application运行:
> java jozart.dynamictree.DynamicTree
远程表服务器和客户端是设计成用RMI分别运行的。但RMI可能难以设置。RMI要求一个RMI registry、一个HTTP服务器和一个安全策略文件。幸运的是,另一个演示程序不那么麻烦,我会先解释怎样运行它。
RemoteTableDemo把一个编辑器、一个查看器和一个服务器都包到了一起。
将RemoteTableDemo作为applet运行:
> appletviewer jozart/remotetabledemo/RemoteTableDemo.html
将RemoteTableDemo作为application运行:
> java jozart.remotetabledemo.RemoteTableDemo
要分别运行服务器和客户端,需要执行以下步骤:
1. 确定rmiregistry正在运行:
> start rmiregistry
2. 确定有一个http服务器正在运行:
> start httpd
3. 换到演示程序的目录。
> cd threads3
4. 启动服务器和客户端:
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableServer
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableEditor
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableViewer
你需要在codebase设置中设置你的正确的主机名。你还应该检查java.policy没有给出过多的权限。
更多关于RMI的介绍请参阅The Java Tutorial(参见"参考资料"一节)。
------------------------------------------------------------------------------
本文演示了两种共同使用线程和基于模型的组件(如JTable和JTree)的方法。并提供了一个修订过的SwingWorker工具类。
在一开头,我说明了构建线程安全的组件是困难的。Java在语言级别提供了对线程和锁的支持,但这并不能使得并发编程突然变得容易起来。Swing的单线程规则使得程序员们的日常任务从线程安全的复杂性中解脱出来,但当确实需要线程时却成了阻碍。
为了使得我自己和我的代码的未来使用者在并发编程上的工作更容易些,我尽可能地使用了工具包、证明有效的设计模式和广泛接受的约定。
在文章的最后,我要给任何有兴趣扩展SwingWorker或者开发他们自己的线程工具的读者提个建议:弄清楚你的内存模型。
关于共享资源的访问控制,我还要再说一下。Java的synchronized语句确保了在线程间传递值的安全。这个重要的细节常常被程序员们忽略。关于Java内存模型的更多信息可以参考Doug Lea对《Concurrent Programming in Java》一书的在线补充。这个参考资料和其他参考资料的链接都列在了下一节。
-------------------------------------------------------------------------------
1. Threads and Swing - Hans Muller and Kathy Walrath.
2. Using a Swing Worker Thread - Hans Muller and Kathy Walrath.
3. Understanding the TreeModel - Eric Armstrong, Tom Santos, Steve Wilson.
4. The JTable Class is DB-Aware - Philip Milne and Mark Andrews.
5. The Java Tutorial on RMI - Ann Wollrath and Jim Waldo.
6. Concurrent Programming in Java - Doug Lea.
7. util.concurrent package - Doug Lea.
-------------------------------------------------------------------------------
Joseph Bowbeer是住在西雅图的一个Java顾问。他对并发编程的着迷使他找准了自己的方向。他要感谢Doug Lea、Tom May、David Holmes和Joshua Bloch,感谢他们分享他们的知识。文中的任何错误和疏忽都归咎于他自己。
使用Swing Worker线程 --执行后台任务的新方法
转贴自:
http://www.softhouse.com.cn/html/200410/2004102610530400001371.html
| 本文给出了一些使用SwingWorker类的例子。SwingWorker类的目的是实现一个后台线程,让你可以用它来执行一些费时的操作,而不影响你的程序的GUI的性能。关于SwingWorker类的一些基本信息,请参阅《线程和Swing》。 注意:在2000年9月我们修改了这篇文章和它的例子以适用于一个更新版本的SwingWorker类。SwingWorker类的这个版本修正了一些微妙的线程bug。 对执行一些费时或可阻塞操作的Swing程序来说,线程是基本的解决之道。例如,如果你的应用程序要根据用户选择的菜单项发出一个数据库请求或加载一个文件,那么你应该在一个单独的线程中完成这些工作。本文阐述了在一个分离的worker线程中完成上述工作的途径。 本文包括以下主要内容:
概览:SwingWorker类因为SwingWorker类并不是Java发行版的一部分,你需要下载和编译它才能使用。它的源代码在这里:SwingWorker.java SwingWorker类简介SwingWorker类可以简单且方便地用于在一个新的线程中计算一个数值。要使用这个类,你只要创建一个SwingWorker的子类并覆盖SwingWorker.construct()方法来执行计算。然后你实例化它,并在这个新实例上调用start()方法。例如,下面的代码片断中产生了一个线程,其中构造了一个字符串。然后,片断中使用了get()方法来取得前面由construct方法所返回的值,并且在必要时将等待。最后,在显示器上显示出字符串的值。 SwingWorker worker = new SwingWorker() { public Object construct() { return "Hello" + " " + "World"; } }; worker.start(); System.out.println(worker.get().toString()); 在实际的应用程序中,construct方法会做些更有用(但可能很费时)的事情。比如,它可能做下列工作之一:
在上面的代码片断中没有展示的一个SwingWorker类的特性是,当construct()返回后,SwingWorker可以让你在事件派发线程中执行一些代码。你可以通过覆盖SwingWorker的finished()方法来做到这一点。典型地,你可以用finished()来显示刚刚构造的一些组件或设置组件上显示的数据。 原始版本的SwingWorker类的一个局限是,一旦一个worker线程开始运行,你无法中断它。(译注:本文、本文的例子及SwingWorker类曾被更新。)不管怎么说,对于一个交互应用程序来说,让用户在工作线程完成前一直等待是相当糟糕的风格。如果用户希望终止一项正在执行中的操作,执行此操作的线程应该能够尽快中止。 使用interrupt()方法在第二版的SwingWorker类中加入了一个interrupt()方法以允许中断一个线程。你的线程应该以下面两种途径之一得到中断的通知:
使用sleep()或wait()方法的工作线程(如后面例子中的线程)一般不需要显式检查是否被中断。通常让sleep()或wait()方法抛出InterruptedException就足够了。 不过,如果你希望能够中断一个不含定时循环的SwingWorker,还是需要用interrupted()方法来显式检查中断。 引入Worker线程的例子
例子的源代码由以下文件组成:
例子一:中断Swing Worker线程接着我们要讨论的例子是Example1.java。这个例子中的worker线程包含一个执行100次的循环,并在两次循环之间睡眠半秒。 例子二:从Worker线程提示用户这个例子实现为Example1的子类。唯一的区别是,在worker线程执行了大约两秒后,它将阻塞直到用户响应一个Continue/Cancel模态对话框。如果用户选择的不是"Continue",我们就退出doWork()循环。
|
| 出处: Java原创社区 FooSleeper 翻译 日期:2004-10-26 |
线程与Swing
转贴自:
http://www.softhouse.com.cn/html/200410/2004102610560200001372.html
| 本文关于Swing中的多线程,发表于1998年4月。一个月后,我们发表了另一篇文章《使用Swing Worker线程》,该文更深入地讨论了这一主题。要更好地了解多线程在Swing中如何工作,我们建议你把这两篇文章都看一下。 注意:在2000年9月我们修改了这篇文章和它的例子以适用于一个更新版本的SwingWorker类。SwingWorker类的这个版本修正了一些微妙的线程bug。 Swing API的设计目标是强大、灵活和易用。特别地,我们希望能让程序员们方便地建立新的Swing组件,不论是从头开始还是通过扩展我们所提供的一些组件。 出于这个目的,我们不要求Swing组件支持多线程访问。相反,我们向组件发送请求并在单一线程中执行请求。 本文讨论线程和Swing组件。目的不仅是为了帮助你以线程安全的方式使用Swing API,而且解释了我们为什么会选择现在这样的线程方案。 本文包括以下内容:
一旦Swing组件被具现化(realized),所有可能影响或依赖于组件状态的代码都应该在事件派发线程中执行。 这个规则可能听起来有点吓人,但对许多简单的程序来说,你用不着为线程问题操心。在我们深入如何撰写Swing代码之前,让我们先来定义两个术语:具现化(realized)和事件派发线程(event-dispatching thread)。 具现化的意思是组建的paint()方法已经或可能会被调用。一个作为顶级窗口的Swing组件当调用以下方法时将被具现化:setVisible(true)、show()或(可能令你惊奇)pack()。当一个窗口被具现化,它包含的所有组件都被具现化。另一个具现化一个组件的方法是将它放入到一个已经具现化的容器中。稍后你会看到一些对组件具现化的例子。 事件派发线程是执行绘制和事件处理的线程。例如,paint()和actionPerformed()方法会自动在事件派发线程中执行。另一个将代码放到事件派发线程中执行的方法是使用SwingUtilities类的invokeLater()方法。 所有可能影响一个已具现化的Swing组件的代码都必须在事件派发线程中执行。但这个规则有一些例外:
大多数初始化后的GUI工作自然地发生在事件派发线程。一旦GUI成为可见,大多数程序都是由事件驱动的,如按钮动作或鼠标点击,这些总是在事件派发线程中处理的。 不过,总有些程序需要在GUI成为可见后执行一些非事件驱动的GUI工作。比如:
下面是一些使用这几个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工作,线程是不必要的,比如对组件属性的更新。 不管怎么说,有时候线程是必要的。下列情况是使用线程的一些典型情况:
你可以使用两个类来帮助你实现线程:
使用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(); } ... } 在一个线程中执行所有的用户界面代码有这样一些优点:
我们的基本信条是,当设计和建造多线程应用程序,尤其是那些包括GUI组件的应用程序时,必须保证极端小心。线程的使用可能会很有欺骗性。在许多情况下,它们表现得能够极好的简化编成,使得设计"专注于单一任务的简单自治实体"成为可能。在一些情况下它们的确简化了设计和编码。然而,在几乎所有的情况下,它们都使得调试、测试和维护的困难大大增加甚至成为不可能。无论大多数程序员所受的训练、他们的经验和实践,还是我们用来帮助自己的工具,都不是能够用来对付非决定论的。例如,全面测试(这总是困难的)在bug依赖于时间时是几乎不可能的。尤其对于Java来说,一个程序要运行在许多不同类型的机器的操作系统平台上,并且每个程序都必须在抢先和非抢先式调度下都能正常工作。 由于这些固有的困难,我们力劝你三思是否绝对有使用线程的必要。尽管如此,有些情况下使用线程是必要的(或者是被其他软件包强加的),所以subArctic提供了一个线程安全的访问机制。本章讨论了这一机制和怎样在一个独立线程中安全地操作交互树。 他们所说的线程安全机制非常类似于SwingUtilities类提供的invokeLater()和invokeAndWait()方法。 |
| 出处: Java原创社区 FooSleeper 翻译 日期:2004-10-26 |
使用JasperReport和iReport制作java报表
使用php实现隐藏文件实际位置的文件下载,
Linux下java中文解决办法