Swing的单线程开发机制
多线程开发,显然要比单线程开发有趣、高效、美妙得多。特别是在Java这种天生支持多线程的语言中,更是如此。可是,Java最重要的组成部分Swing确是单线程的!
并非只有Swing是单线程的,大多数GUI库都是单线程的。因为,在GUI的事件处理中,事件和处理事件的底层资源是如此的复杂,以至于使用多线程开发,很难避免死锁和资源竞争问题的发生。而且,如果锁定太多系统资源,对GUI的系统性能将会造成消极影响。
因此,Swing被开发成了一个基于事件队列的单线程编程模型。GUI上的事件,一个个依次在“事件派发线程”上执行,不会发生事件对资源的争夺。
Java.awt.EventQueue类,就执行这个功能。
EventQueue 是一个与平台无关的类,它将来自于基础同位体类和受信任的应用程序类的事件列入队列。
它封装了异步事件指派机制,该机制从队列中提取事件,然后通过对此 EventQueue 调用 dispatchEvent(AWTEvent)
按顺序。
也就是说,不允许同时从该队列中指派多个事件。
指派顺序与它们排队的顺序相同。
也就是说,如果 AWTEvent A 比 AWTEvent B 先排入到 EventQueue 中,那么事件 B 不能在事件 A 之前被指派。
一些浏览器将不同基本代码中的 applet 分成独立的上下文,并在这些上下文之间建立一道道墙。在这样的场景中,每个上下文将会有一个 EventQueue。其他浏览器将所有的 applet 放入到同一个上下文中,这意味着所有 applet 只有一个全局 EventQueue。该行为是与实现有关的。有关更多信息,请参照浏览器的文档。
所有Swing/AWT事件的处理方法,都被放到唯一的“事件派发线程”中执行。
一般,我们使用EventQueue类的2个方法,将事件处理方法放到“事件派发线程”中执行。
invokeLater 和 invokeAndWait
设计Swing的UI组件的执行,一般都需要运行在“事件派发线程”上。
Swing单线程开发引起的问题
Java是一种多线程编程语言。多线程给程序带来了并发的好处。Swing单线程开发的一个问题是,如果在“事件派发线程”上执行的运算太多,那么GUI界面就会停住,系统响应和运算就会非常缓慢。
既然,“事件派发线程”是为了处理GUI事件而设的,那么,我们只应该把GUI事件处理相关的代码,放在“事件派发线程”中执行。其他与界面无关的代码,应该放在Java其他的线程中执行。
这样,我们在Swing的事件处理中,仍然使用Swing的单线程编程模型,而其他业务操作均使用多线程编程模型,这就可以大大提高Swing程序的响应和运行速度,充分运用Java多线程编程的优势。
下面用实例慢慢的接触这个过程
:
现在我们要做一个简单的界面。
包括一个进度条、一个输入框、开始和停止按钮。
需要实现的功能是:
当点击开始按钮,则更新进度条,并且在输入框内把完成的百分比输出(这里只做例子,没有真正去做某个工作)。
1 package test;
2
3 import java.awt.FlowLayout;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6
7 import javax.swing.JButton;
8 import javax.swing.JFrame;
9 import javax.swing.JProgressBar;
10 import javax.swing.JTextField;
11
12 public class SwingThreadTest1 extends JFrame {
13 private static final long serialVersionUID = 1L;
14 private static final String STR = "Completed : ";
15 private JProgressBar progressBar = new JProgressBar();
16 private JTextField text = new JTextField(10);
17 private JButton start = new JButton("Start");
18 private JButton end = new JButton("End");
19 private boolean flag = false;
20 private int count = 0;
21
22 public SwingThreadTest1() {
23 this.setLayout(new FlowLayout());
24 add(progressBar);
25 text.setEditable(false);
26 add(text);
27 add(start);
28 add(end);
29 start.addActionListener(new Start());
30 end.addActionListener(new End());
31 }
32
33 private void go() {
34 while (count < 100) {
35 try {
36 Thread.sleep(100);// 这里比作要完成的某个耗时的工作
37 } catch (InterruptedException e) {
38 e.printStackTrace();
39 }
40 // 更新进度条和输入框
41 if (flag) {
42 count++;
43 progressBar.setValue(count);
44 text.setText(STR + String.valueOf(count) + "%");
45 }
46 }
47 }
48
49 private class Start implements ActionListener {
50 public void actionPerformed(ActionEvent e) {
51 flag = true;// 设置开始更新的标志
52 go();// 开始工作
53 System.out.println(Thread.currentThread().getName());
54 }
55 }
56
57 private class End implements ActionListener {
58 public void actionPerformed(ActionEvent e) {
59 flag = false;// 停止
60 }
61 }
62
63 public static void main(String[] args) {
64 SwingThreadTest1 fg = new SwingThreadTest1();
65 fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
66 fg.setSize(300, 100);
67 fg.setVisible(true);
68 }
69 }
View Code
运行代码发现,
现象1:当点击了开始按钮,画面就卡住了。按钮不能点击,进度条没有被更新,输入框上也没有任何信息。
原因分析:Swing是线程不安全的,是单线程的设计,所以只能从事件派发线程访问将要在屏幕上绘制的Swing组件。ActionListener的actionPerformed方法是在事件派发线程中调用执行的,而点击了开始按钮后,执行了go()方法,在go()里,虽然也去执行了更新组件的方法
progressBar.setValue(count);
text.setText(STR + String.valueOf(count) + "%");
但由于go()方法直到循环结束,它并没有返回,所以更新组件的操作一直没有被执行,这就造成了画面卡住的现象。
go方法也一直在事件派发进程上,和更新UI在一个进程中,在一个进程中就必然是顺序执行的了。
现象2:过了一段时间(go方法里的循环结束了)后,画面又可以操作,并且进度条被更新,输入框也出现了我们想看到的信息。
原因分析:通过在现象1的分析,很容易联想到,当go()方法返回了,则其他的线程(更新组件)可以被派发了,所以画面上的组件被更新了。
为了让画面不会卡住,我们来修改代码,将耗时的工作放在一个线程里去做。
1 package test;
2
3 import java.awt.FlowLayout;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6
7 import javax.swing.JButton;
8 import javax.swing.JFrame;
9 import javax.swing.JProgressBar;
10 import javax.swing.JTextField;
11
12 public class SwingThreadTest2 extends JFrame {
13 private static final long serialVersionUID = 1L;
14 private static final String STR = "Completed : ";
15 private JProgressBar progressBar = new JProgressBar();
16 private JTextField text = new JTextField(10);
17 private JButton start = new JButton("Start");
18 private JButton end = new JButton("End");
19 private boolean flag = false;
20 private int count = 0;
21
22 GoThread t = null;
23
24 public SwingThreadTest2() {
25 this.setLayout(new FlowLayout());
26 add(progressBar);
27 text.setEditable(false);
28 add(text);
29 add(start);
30 add(end);
31 start.addActionListener(new Start());
32 end.addActionListener(new End());
33 }
34
35 private void go() {
36 while (count < 100) {
37 try {
38 Thread.sleep(10);
39 } catch (InterruptedException e) {
40 e.printStackTrace();
41 }
42 if (flag) {
43 count++;
44 System.out.println(count);
45 progressBar.setValue(count);
46 System.out.println(Thread.currentThread().getName());
47 text.setText(STR + String.valueOf(count) + "%");
48 }
49 }
50 }
51
52 private class Start implements ActionListener {
53 public void actionPerformed(ActionEvent e) {
54 System.out.println(Thread.currentThread().getName());
55 flag = true;
56 if (t == null) {
57 t = new GoThread();
58 t.start();
59 }
60 }
61 }
62
63 // 执行复杂工作,然后更新组件的线程
64 class GoThread extends Thread {
65 public void run() {
66 // do something...
67 go();
68 }
69 }
70
71 private class End implements ActionListener {
72 public void actionPerformed(ActionEvent e) {
73 flag = false;
74 }
75 }
76
77 public static void main(String[] args) {
78 SwingThreadTest2 fg = new SwingThreadTest2();
79 fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
80 fg.setSize(300, 100);
81 fg.setVisible(true);
82 }
83 }
View Code
我们执行了程序,结果和我们想要的一样,画面不会卡住了。
那这个程序是否没有问题了呢?
我们自定义了一个线程GoThread,在这里我们完成了那些耗时的工作,可以看作是“工作线程”,
而对于组件的更新,我们也放在了“工作线程”里完成了。
在这里,在事件派发线程以外的线程里设置进度条,是一个危险的操作,运行是不正常的。(对于输入框组件的更新是安全的。)
只有从事件派发线程才能更新组件,根据这个原则,我们来修改我们现有代码。
1 package test;
2
3 import java.awt.FlowLayout;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ActionListener;
6
7 import javax.swing.JButton;
8 import javax.swing.JFrame;
9 import javax.swing.JProgressBar;
10 import javax.swing.JTextField;
11 import javax.swing.SwingUtilities;
12
13 public class SwingThreadTest3 extends JFrame {
14 private static final long serialVersionUID = 1L;
15 private static final String STR = "Completed : ";
16 private JProgressBar progressBar = new JProgressBar();
17 private JTextField text = new JTextField(10);
18 private JButton start = new JButton("Start");
19 private JButton end = new JButton("End");
20 private boolean flag = false;
21 private int count = 0;
22
23 private GoThread t = null;
24
25 private Runnable run = null;// 更新组件的线程
26
27 public SwingThreadTest3() {
28 this.setLayout(new FlowLayout());
29 add(progressBar);
30 text.setEditable(false);
31 add(text);
32 add(start);
33 add(end);
34 start.addActionListener(new Start());
35 end.addActionListener(new End());
36
37 run = new Runnable() {// 实例化更新组件的线程
38 public void run() {
39 System.out.println(Thread.currentThread().getName());
40 progressBar.setValue(count);
41 text.setText(STR + String.valueOf(count) + "%");
42 }
43 };
44 }
45
46 private void go() {
47 while (count < 100) {
48 try {
49 Thread.sleep(10);
50 } catch (InterruptedException e) {
51 e.printStackTrace();
52 }
53 if (flag) {
54 count++;
55 System.out.println(Thread.currentThread().getName());
56 SwingUtilities.invokeLater(run);// 将对象排到事件派发线程的队列中
57 }
58 }
59 }
60
61 private class Start implements ActionListener {
62 public void actionPerformed(ActionEvent e) {
63 flag = true;
64 if (t == null) {
65 t = new GoThread();
66 t.start();
67 }
68 }
69 }
70
71 class GoThread extends Thread {
72 public void run() {
73 // do something...
74 go();
75 }
76 }
77
78 private class End implements ActionListener {
79 public void actionPerformed(ActionEvent e) {
80 flag = false;
81 }
82 }
83
84 public static void main(String[] args) {
85 SwingThreadTest3 fg = new SwingThreadTest3();
86 fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
87 fg.setSize(300, 100);
88 fg.setVisible(true);
89 }
90 }
View Code
解释:SwingUtilities.invokeLater()方法使事件派发线程上的可运行对象排队。当可运行对象排在事件派发队列的队首时,就调用其run方法。其效果是允许事件派发线程调用另一个线程中的任意一个代码块。
还有一个方法SwingUtilities.invokeAndWait()方法,它也可以使事件派发线程上的可运行对象排队。
区别:
下面这个是弹出个alert窗口。若用invokeAndWait(),那么打印一段文字将在你点击了OK buton之后才会执行,而如用invokeLater()则,立马后执行输出操作。
1 package test;
2
3 import java.lang.reflect.InvocationTargetException;
4
5 import javax.swing.JOptionPane;
6 import javax.swing.SwingUtilities;
7
8 public class Invoke {
9 public static void main(String[] args) {
10 Runnable showModalDialog = new Runnable() {
11 public void run() {
12 JOptionPane.showMessageDialog(null, "No active shares found on this IP!");
13 }
14 };
15 try {
16 SwingUtilities.invokeAndWait(showModalDialog);
17 System.out.println("sssssssssss");
18 } catch (InterruptedException e) {
19 // TODO Auto-generated catch block
20 e.printStackTrace();
21 } catch (InvocationTargetException e) {
22 // TODO Auto-generated catch block
23 e.printStackTrace();
24 }
25 }
26 }
View Code