Swing多线程

Swing的单线程开发机制
多线程开发,显然要比单线程开发有趣、高效、美妙得多。特别是在Java这种天生支持多线程的语言中,更是如此。可是,Java最重要的组成部分Swing确是单线程的!
并非只有Swing是单线程的,大多数GUI库都是单线程的。因为,在GUI的事件处理中,事件和处理事件的底层资源是如此的复杂,以至于使用多线程开发,很难避免死锁和资源竞争问题的发生。而且,如果锁定太多系统资源,对GUI的系统性能将会造成消极影响。
因此,Swing被开发成了一个基于事件队列的单线程编程模型。GUI上的事件,一个个依次在“事件派发线程”上执行,不会发生事件对资源的争夺。
Java.awt.EventQueue类,就执行这个功能。
EventQueue 是一个与平台无关的类,它将来自于基础同位体类和受信任的应用程序类的事件列入队列。
它封装了异步事件指派机制,该机制从队列中提取事件,然后通过对此 EventQueue 调用 dispatchEvent(AWTEvent) 方法来指派这些事件(事件作为参数被指派)。该机制的特殊行为是与实现有关的。指派实际排入到该队列中的事件(注意,正在发送到 EventQueue 中的事件可以被合并)的惟一要求是:
按顺序。
也就是说,不允许同时从该队列中指派多个事件。
指派顺序与它们排队的顺序相同。
也就是说,如果 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
原文地址:https://www.cnblogs.com/fengjian/p/3476919.html