SwingWorker应用详解
前续知识,一个swing程序有三个类型的线程,
- 初始化线程:就是main函数,用来启动GUI
- 用户事件调度线程EDT:负责对GUI组件的渲染和刷新,它只有一个,一定要注意这个问题,它处理的就是事件队列里的事情,他通过调用事件处理器来响应用户交互。所有的事件处理都是在EDT上进行的。
- 任务线程:响应时具体的数据处理。
Swing框架负责管理组件绘制、更新以及EDT上的线程处理。可以想象,该线程的事件队列很繁忙,几乎每一次GUI交互和事件都是通过它完成。事件队列的上任务必须非常快,否则就会阻塞其他任务的执行,使队列里阻塞了很多等待执行的事件,造成界面响应不灵活,让用户感觉到界面响应速度很慢,使他们失去兴趣。理想情况下,任何需时超过30到100毫秒的任务不应放在EDT上执行,否则用户就会觉察到输入和界面响应之间的延迟。
在这里一定注意:
- 其他线程上访问UI组件和事件处理器都是不安全的,都有可能导致界面的更新和绘制错误。
- 在EDT上执行耗时性任务会发生阻塞队列,会让你觉得界面很卡,所以,要尽可能的不要在EDT上编写耗时性的代码。
- 应当使用独立的任务线程来执行耗时性的任务。
所以到现在一句话,所有耗时性的任务(即任何干扰或延迟UI的任务)都应该放在任务线程中去解决。在初始化线程和任务线程中与UI组件或其缺省数据模型进行的交互都是非线程安全的。
既然是非线程安全怎么办呢?这时候一个非常强大的类就出现了,神一般的SwingWorker工具类,它帮助任务线程与EDT进行交互,虽然不能实现完全的线程安全,但是解决了大部分的非线程安全的问题。通过它使任务线程和EDT各负其责,EDT实现绘制和刷新,任务线程实现与界面无关的耗时性的操作。将EDT线程仅用于GUI任务。
在这里加个知识点:
同步与异步:
- 同步是程序在发起请求后开始处理事件并等待处理的结果或等待请求执行完毕,在此之前程序被block住直到请求完成。
- 异步是当前程序发起请求后立即返回,当前程序不会立即处理该事件并等待处理的结果,请求是在稍后的某一时间才被处理。
串行与并行:
- 串行是指多个要处理请求顺序执行,处理完一个再处理下一个;容易阻塞。
- 并行可以理解为并发,是同时处理多个请求(实际上我们只能理解为是这样,特别是CPU数目少于线程数的机器而言,真正意义的并发是不存在的,各个线程只是断断续续地交替地执行)。
SwingWorker类介绍
Class SwingWorker<T,V>
参数类型
T - 该 SwingWorker's doInBackground和 get方法返回的结果类型,就是最终结果的类型
V - 用于执行中间结果的类型 SwingWorker's publish和 process方法的处理的数据类型 ,就是中间结果的类型
All Implemented Interfaces:Runnable, Future<T>, RunnableFuture<T>
最重要的几个方法:
protected abstract T doInBackground()
- 计算一个结果,如果不能这样做,就会抛出一个异常。
- 首先,这个方法一定要重写,因为这里是处理一个swingworker实例的最后计算结果,当这个函数被执行完后,说明这个线程的所有顺序执行的程序已经执行完了。
- 其次,想要获取doinbackground方法的返回值要使用get方法,get方法是同步的,所以一般使用时在done方法中调用或者在判断isdone后执行。
- 最后,因为,当doinbackground执行完后,在EDT中会自动执行done方法。
protected void done()
doInBackground方法完成后,在EDT中被执行。
void execute()
计划这个 SwingWorker在 工作线程上执行。用它来开启这个进程,就是说一旦执行这个方法,swingworker就进入了工作线程。
T get()
等待doInBackground计算完成,返回doInBackground的返回值。
boolean isDone()
判断是否完成整个计算过程,如果此任务完成,则返回 true 。
protected void process(List<V> chunks)
在 事件调度线程上异步接收来自 publish方法的数据块。即中间结果。中间结果是任务线程在产生最后结果之前就能产生的数据。当任务线程执行时,它可以发布类型为V的中间结果,通过覆盖process方法来处理中间结果。任务对象的父类会在EDT线程上激活process方法,因此在process方法中程序可以安全的更新UI组件。这个方法线程安全的实现了DET与任务线程之间的交互。过程是这样的,当从任务线程调用publish方法时,在DET上自动执行process方法。
protected void publish(V chunks)
发送数据chunks到 process(java.util.List<V>)方法中的list列表中。这个方法是自动在DET上被调用的。是由任务对象的父类将其激活的。
SwingWorker具体过程讲解:
首先我们要知道一个SwingWorker的生命周期涉及三个状态,三个线程:
- PENDING:初始状态,被创建时的状态。
- STARTED:调用 doInBackground后的状态。
- DONE:doInBackground方法完成后的状态。
- 当前线程:在这个线程上调用了execute()方法。它在工作线程上执行SwingWorker,并立即返回,因为是异步的。
- 工作线程:在这个线程上调用了doInBackground()方法。 这是所有背景活动都应该发生的地方。
- 事件调度线程EDT:所有Swing相关活动发生在此线程上。
知道以上状态和涉及的线程后,我们看整个SwingWorker的运行过程:
一般当前线程是EDT,当在当前线程中执行excute方法后,在工作线程上执行swingworker(注意为什么不是直接进入工作线程呢,是因为是并发执行的,具体什么时候看CPU),即调用doInBackground方法,这时候,状态由PENDING进入STARTED。在此方法中,如果希望获得中间结果,可以用publish(V chunk)方法,chunk是中间结果,一旦publish,就会将chunk送入process(List<V> chunks)方法中的chunks列表中,并且这个process方法在EDT中自动执行,这个方法非常漂亮的实现了任务线程与EDT之间的交互。当doInBackground方法执行完后,会使状态值STARTED变为DONE状态,并且自动执行在EDT中自动执行done方法,注意done是在EDT中执行的,所以这里也实现了交互,在done方法里调用get方法获取doInBackground的返回值。到此,整个swingworker就被执行完了,而且它只能够执行一次。
这里有一个问题:为什么直接通过调用doInBackground或许返回值呢,答案是当然不行呀,其实这个方法也是自动被调用的,不是自己调用的呀,这个方法是由swingworker内部调用的,精髓就在这里呀,就是因为这个方法是swingworker自己为了实现异步而自己去调用的,我们只是去重写它去实现相应的计算。而且,这个方法只能运行一次,wingworker在设计上只能被运行一次就是因为这个原因。所以想要获取这个方法的返回值,我们只能用同步的get方法。既然是同步的所以会发生阻塞现象,提前要判断isdone了没。
总结一下,doInBackground、publish是任务线程中调用的,done、process是在EDT中调用的的。doInBackground与done,publish与process,这两对方法,优美的完成了大部分EDT与任务线程之间的线程安全的异步交互。记得,EDT上执行的代码都尽可能是非耗时性的。
运行实例一:实现工作线程与EDT线程安全的并发执行
在航显系统中,创建一个JTable列表,每当我们从服务器获取一个事件信息后,我们需要处理这条数据信息,并把它添加到列表中,如果此时不进行线程安全的并发操作,那么UI产生的其它事件都阻塞在EDT的事件触发队列里,什么操作都做不了,知道数据全部处理完。现在我们用SwingWorker来实现就不会出现这种问题。
首先将所有后台处理数据放在doInBackground中执行,即重写。
1 @Override
2 protected String doInBackground() throws Exception {
3
4 String dateLines = null;
5 Pattern pattern = Pattern.compile(
6 "(?mx)^.*flid=([^0]|[0-9]{2,}),.*flno=(.*),.*mvin=D.*"+
7 "("+
8 "(Rout.*rtno=) (1)"+
9 "(,\\sapcd=) (\\w{3})"+
10 "(,\\s(\\w|=|,|\\s)*scdt=) ([\\d|\\w]{0,11})"+
11 "(\\],\\s)"+
12 ")"+
13 "("+
14 "(Rout.*rtno=) (2)"+
15 "(,\\sapcd=) (\\w{3})"+
16 "(,\\s(\\w|=|,|\\s)*scdt=) ([\\d|\\w]{0,11})"+
17 "(\\],\\s)"+
18 ")"+
19 "(("+
20 "(Rout.*rtno=) (3)"+
21 "(,\\sapcd=) (\\w{3})"+
22 "(,\\s(\\w|=|,|\\s)*scdt=) ([\\d|\\w]{0,11})"+
23 "(\\],\\s)"+
24 ")|)(.*)"
25 );
26 try (Socket socket = new Socket("10.5.25.193", 9999)) {
27
28 Scanner scanner = new Scanner(socket.getInputStream());
29 String totalLines = scanner.nextLine();
30 System.out.println(totalLines);
31 JOptionPane.showMessageDialog(null, "连接服务器成功\r\n" + totalLines );
32 while (scanner.hasNextLine()) {
33 dateLines = scanner.nextLine();
34 Matcher matcher = pattern.matcher(dateLines);
35 if (matcher.find()) {
36 String [] strings =new String[]{
37 matcher.group(1+1),
38 getAirportName(matcher.group(6+1)),
39 getAirportName(matcher.group(15 + 1)),
40 getAirportName(matcher.group(25 + 1)),
41 getTime(matcher.group(9 + 1)),
42 };
43 publish(strings);
44 }
45 }
46 }
47 return "dates have been processed!";
1 return "dates have been processed!"
48 }
由于此方法是异步执行所以会直接返回值,在后续过程中继续执行。由于我们希望没处理完一条信息,就显示在航显系统中,所以我们需要的是过程中的处理结果,将过程中的处理结果publish到process维护的数据块中。每当publish方法在任务线程中执行一次,process方法就会在EDT中被执行。但是process从publish获得的数据是以数据列表形式被存储的,此列表中包含了所有publish的数据。现在我们就可以重写process方法,实现工作线程与EDT的交互。每次我们将chunks中的最后一条数据添加到table中。
1 protected void process(List<String[]> chunks) {
2 tableModel.addRow(chunks.get(chunks.size()-1));
3 }
当希望所有的数据处理完后,即doInBackground方法执行完,即工作线程结束后,需要收尾工作,那么我们就可以重写done方法。
1 protected void done() {
2 String message;
3 try {
4 message = get();//得到doinbackground方法的返回值
5 JOptionPane.showMessageDialog(null, message);
6 } catch (ExecutionException | InterruptedException e) {
7 e.printStackTrace();
8 }
9 }
通过SwingWorker很好了实现了任务线程与EDT之间线程安全的异步的数据交互。
实例二:通过SwingWorker实现JProgressBar
1 import java.awt.BorderLayout;
2 import java.awt.EventQueue;
3 import java.util.List;
4 import javax.swing.JFrame;
5 import javax.swing.JOptionPane;
6 import javax.swing.JPanel;
7 import javax.swing.border.EmptyBorder;
8 import javax.swing.JProgressBar;
9 import javax.swing.SwingWorker;
10 import javax.swing.JButton;
11 import java.awt.event.ActionListener;
12 import java.awt.event.ActionEvent;
13
14 public class SwingWorkerWithProgressBar extends JFrame {
15
16 private JPanel contentPane;
17 private JProgressBar progressBar;
18
19 /**
20 * Launch the application.
21 */
22 public static void main(String[] args) {
23 EventQueue.invokeLater(new Runnable() {
24 public void run() {
25 try {
26 SwingWorkerWithProgressBar frame = new SwingWorkerWithProgressBar();
27 frame.setVisible(true);
28 } catch (Exception e) {
29 e.printStackTrace();
30 }
31 }
32 });
33 }
34
35 /**
36 * Create the frame.
37 */
38 public SwingWorkerWithProgressBar() {
39 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
40 setBounds(100, 100, 450, 300);
41 contentPane = new JPanel();
42 contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
43 contentPane.setLayout(new BorderLayout(0, 0));
44 setContentPane(contentPane);
45
46 progressBar = new JProgressBar(0, 100);
47 contentPane.add(progressBar, BorderLayout.NORTH);
48
49 JButton btnBegin = new JButton("Begin");
50 btnBegin.addActionListener(new ActionListener() {
51 public void actionPerformed(ActionEvent e) {
52 new ProgressBarRealized().execute();
53 }
54 });
55 contentPane.add(btnBegin, BorderLayout.SOUTH);
56 }
57
58 class ProgressBarRealized extends SwingWorker<Void, Integer> {
59
60 @Override
61 //后台任务在此方法中实现
62 protected Void doInBackground() throws Exception {
63 // 模拟有一百项任务,每次睡1s
64 for (int i = 0; i < 100; i++) {
65 Thread.sleep(1000);
66 publish(i);//将当前进度信息加入chunks中
67 }
68 return null;
69 }
70 @Override
71 //每次更新进度条的信息
72 protected void process(List<Integer> chunks) {
73 progressBar.setValue(chunks.get(chunks.size() - 1));
74 }
75 @Override
76 //任务完成后返回一个信息
77 protected void done() {
78 JOptionPane.showMessageDialog(null, "任务完成!");
79 }
80 }
81 }
关于Swing的线程安全
SwingWorker
适用于需要在后台线程中运行长时间运行的任务并在完成或处理时向UI提供更新的情况。 SwingWorker
子类必须实现doInBackground()方法来执行后台计算。大多数Swing API是非线程安全的,也就是说不能在任意地方调用,它应该只在EDT中调用。Swing的线程安全靠事件队列和EDT来保障。对非EDT的并发调用需通过 invokeLater(runnable)和invokeAndWait(runnable)使请求插入到队列中等待EDT去执行。 invokeLater(runnable)方法是异步的,它会立即返回,具体何时执行请求并不确定,所以命名invokeLater是稍后调用。invokeAndWait(runnable)方法是同步的,它被调用结束会立即block当前线程(调用invokeAndWait的那个线程)直到EDT处理完那个请求。invokeAndWait一般的应用是取得Swing组件的数据。
参考博文:http://blog.sina.com.cn/s/blog_4b6047bc010007th.html