SwingUtilities 和线程安全
多线程是图形界面程序的本质特征,几乎所有图形界面程序都要响应按钮、文本框、菜单等控件触发的事件,为了完成这些任务,必须使用多线程。
对于不熟悉多线程程序的人来说,需要一段时间来理解和适应多线程的程序设计。
Swing规范要求,在Swing控件可见以后,所有对其外观的修改都应当采用如下的形式:
SwingUtilities.invokeLater(new Runnable(){
public void run() {
// ...
// 修改GUI控件的外观
}
}) ;
并说明,“由于swing不是线程安全的,不采用这种方式可能会出现意想不到的错误...“
对于不太了解Swing的人来说,这个要求真可以说是莫名其妙,在我对此感到莫名其妙的时候,我也比较好奇,会产生什么意想不到的错误呢?
要说明这个问题,可以用一个具体的例子配合实验来说明。 假设我们用swing实现如下一个应用:
名称:通知
说明:这个swing程序将在启动后,访问网络,将网络上以某种加密格式存储的通知信息显示出来。
界面示例:
上图中"用户需要的数据"就是通知内容。
经过研究,我们决定使用JTextArea作为显示通知的控件,从网络上拿到通知文本以后,使用如下JTextArea指令将通知内容显示出来:
data.setText(str) ;
一个看起来很简单的应用。 可是在我们进行了实现,并做了一些优化以后,却发现时不常的,我们的通知内容不能显示出来,初步调查显示,在无法显示通知时,java控制台会出现一条异常:
这也许就是传说中的意想不到的错误。
下面我们将使用最多不超过80行的本地程序来模拟这个错误的呈现。
最初我们做了如下实现:
import javax.swing.*;
public class taskman_serial {
taskman_serial() {
// 从网上获取通知内容
String str = "这是用户需要的数据" ;
JTextArea data = new JTextArea(str) ;
JFrame frame = new JFrame("任务人II") ;
frame.setSize(300, 200) ;
frame.setDefaultCloseOperation
(JFrame.EXIT_ON_CLOSE) ;
frame.setContentPane(data) ;
frame.setVisible(true) ;
}
public static void main(String[] args)
throws Exception{
String lnf = UIManager
.getCrossPlatformLookAndFeelClassName() ;
UIManager.setLookAndFeel(lnf) ;
JFrame.setDefaultLookAndFeelDecorated(true) ;
new taskman_serial() ;
}
}
运行这个程序,看起来完美的实现了设计要求。
但是,在用户使用了一段时间以后,却提出了一个设计要求之外的需求:
有时候,获取网络上的通知信息需要十几秒甚至更长的时间,而在这段时间内,屏幕一直没有反映,让人感觉程序没有启动,结果重复的双击图标,一会儿出现了一大堆通知窗口。
ok,我们改进程序如下:
1、启动后立即显示程序窗口,通知区域显示”正在获取通知“;
2、装载通知以后,再将通知显示出来
看来肯定需要线程来帮忙了,
1、我们分别启动两个线程,一个获取数据,一个创建gui,
2、GUI创建完成以后马上显示出来,而得到数据以后,再更新数据区域,
3、为了使两个线程都可以访问JTextArea,我们把它定义为taskman的属性变量
为了模拟耗时的网络操作,我们编写了子程序op,用于模拟访问网络消耗的毫秒数:
void op(long millis) {
try {
Thread.sleep(millis) ;
} catch (Exception exc) {}
}
完整代码如下:
import javax.swing.*;
public class taskman_thread {
JTextArea data ;
taskman_thread() {
Thread t_ui = new Thread() {
public void run() {
make_ui() ;
}
} ;
Thread t_load = new Thread() {
public void run() {
load_data() ;
}
} ;
t_ui.start() ;
t_load.start() ;
}
void make_ui() {
data = new JTextArea("正在获取数据...") ;
JFrame frame = new JFrame("任务人II") ;
frame.setContentPane(data) ;
frame.setDefaultCloseOperation
(JFrame.EXIT_ON_CLOSE) ;
frame.setSize(300, 200) ;
frame.setVisible(true) ;
}
void load_data() {
// 用3000毫秒时间访问网络,将得到的数据显示在控件中
op(3000) ;
data.setText("这是用户需要的数据") ;
}
void op(long millis) {
try {
Thread.sleep(millis) ;
} catch (Exception exc) {}
}
public static void main(String[] args)
throws Exception{
String lnf = UIManager
.getCrossPlatformLookAndFeelClassName() ;
UIManager.setLookAndFeel(lnf) ;
JFrame.setDefaultLookAndFeelDecorated(true) ;
new taskman_thread() ;
}
}
经运行,达到预期要求,程序在启动后立即显示了GUI画面,并在稍后完成了网络访问,刷新了通知区域
噩梦是在一个管理需求后开始的,需求说,程序需要在启动后,要到中心服务器获取用户的权限,如果用户可以发布通知,界面上要有发布通知的控件,否则,界面仍然和原来一样。
只须在子程序make_ui()的首部增加语句op(4000),就可以模拟这个问题:
void make_ui() {
op(4000) ;
// 获取用户权限信息
data = new JTextArea("正在获取数据...") ;
JFrame frame = new JFrame("任务人II") ;
frame.setContentPane(data) ;
frame.setDefaultCloseOperation
(JFrame.EXIT_ON_CLOSE) ;
frame.setSize(300, 200) ;
frame.setVisible(true) ;
}
修改其中op()的参数,如果是2000,肯定不会有问题,如果大于3000,就会出问题。
问题显而易见,由于make_ui的执行时间延长了,当得到了数据但JTextArea还未完成初始化时,就会出现问题。
好了,如果是这样,该用SwingUtilities了,是否应该这样写代码:
taskman_ok() {
Thread t_ui = new Thread() {
public void run() {
make_ui() ;
}
} ;
Thread t_load = new Thread() {
public void run() {
load_data() ;
}
} ;
SwingUtilities.invokeLater(t_ui);
SwingUtilities.invokeLater(t_load) ;
}
运行一下结果正常了,时间长了很多,原来4秒可以完成通知的下载和显示,现在需要7秒了,make_ui()和load_data()实际上又变成了串行执行。
再改进,仅把与GUI相关的指令序列放进SwingUtilities,这回OK了,既可以最快的打开界面,又可以并行的获取数据了。
完整代码如下:
import javax.swing.*;
public class taskman_ok {
JTextArea data ;
taskman_ok() {
Thread t_ui = new Thread() {
public void run() {
make_ui() ;
}
} ;
Thread t_load = new Thread() {
public void run() {
load_data() ;
}
} ;
SwingUtilities.invokeLater(t_ui);
t_load.start() ;
}
void make_ui() {
op(4000) ;
// 获取用户权限信息
data = new JTextArea("正在获取数据...") ;
JFrame frame = new JFrame("任务人II") ;
frame.setContentPane(data) ;
frame.setDefaultCloseOperation
(JFrame.EXIT_ON_CLOSE) ;
frame.setSize(300, 200) ;
frame.setVisible(true) ;
}
void load_data() {
// 用3000毫秒时间访问网络,将得到的数据显示在控件中
op(3000) ;
SwingUtilities.invokeLater(new Runnable(){
public void run() {
data.setText("这是用户需要的数据") ;
}
}) ;
}
void op(long millis) {
try {
Thread.sleep(millis) ;
} catch (Exception exc) {}
}
public static void main(String[] args)
throws Exception{
String lnf = UIManager
.getCrossPlatformLookAndFeelClassName() ;
UIManager.setLookAndFeel(lnf) ;
JFrame.setDefaultLookAndFeelDecorated(true) ;
new taskman_ok() ;
}
}