在介绍swing线程机制之前,先介绍一些背景概念。
背景概念
同步与异步:
同步是指程序在发起请求后开始处理事件并等待处理的结果或等待请求执行完毕,在此之前程序被阻塞(block)直到请求完成。
异步是当前程序发起请求后立即返回,当前程序不会立即处理该事件并等待处理的结果,请求是在稍后的某一时间才被处理。
串行与并行:
串行是指多个要处理的请求按照顺序执行,处理完一个再处理下一个。
并行可以理解为并发,指的是同时处理多个请求(实际上我们只能理论上这么理解,特别是CPU数目少于线程数的机器而言,真正意义的并发是不存在的,各个线程只是断断续续地交替执行(以消耗CPU时间片的形式))。
队列:
队列是一种线性的数据结构,元素遵守“先进先出”原则。队列的处理方式是处理完一个再处理下一个。
Swing程序中的线程
一个swing程序包含三种类型的线程:初始化线程(Initial Thread)、事件分派线程(Event Dispatch Thread)和任务线程(Worker Thread)。
初始化线程:每个程序都有一个main方法,这是程序执行的入口,该方法运行在初始化线程上。初始化线程读取程序参数并初始化一些对象。在许多Swing程序中,该线程主要目的是启动程序的图形用户界面(GUI)。一旦GUI启动后,对于大多数事件驱动的桌面程序来说,初始化线程的工作就结束了,程序的控制权就交给了UI。
事件分派线程(EDT ) :主要负责GUI组件的绘制和更新,并响应用户的输入。每个EDT都会负责管理一个事件队列(EventQueue),用户每次对界面更新的请求(包括键盘、鼠标等事件)都会排到事件队列中,然后等待EDT的处理。
任务线程:主要负责执行和界面无直接关系的耗时任务和输入/输出密集型操作,即任何干扰或延迟UI事件的处理都应该由任务线程来完成。注意,任务线程是通过javax.swing.SwingWorker类显式启动的。
EDT线程注意事项
一、任何GUI的请求都必须由EDT线程来处理
EDT线程将所有的GUI组件绘制和更新请求以及事件请求都放入了一个事件队列中。队列这种数据结构前面也讲过了,它是线性、“先进先出”的。所以,通过事件队列的机制,就可以将并发的GUI请求转化为事件队列,从而按顺序处理。这样就可以保证线程安全。所以说,尽管大多数swing API本身不是线程安全的,但是swing通过EDT线程和事件队列机制实现了保障线程安全。
同理,不建议从其他线程直接访问UI组件及其事件处理器,这会破坏线程安全的保障,可能会导致界面更新和绘制错误。
二、在非EDT线程中通过invokeLater和invokeAndWait方法向EDT线程的事件队列添加GUI请求
有的时候需要在一个非EDT线程中调用swing API来处理GUI请求,根据第一条注意事项,显然我们不能直接访问GUI组件。这时候我们可以调用这两个方法将GUI请求添加到EDT线程的事件队列中。
举个例子:我们有一个类A继承了JFrame,如何在main方法中正确的启动GUI呢?我们知道main方法属于初始化线程,这就是典型的非EDT线程访问GUI组件的问题。
错误的启动方式为: new A();
如果这么做了,就相当于在非EDT线程中直接访问了GUI组件,这破坏了线程安全。
正确的启动方式为:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createGUI();
}
});
通过invokeLater和invoke方法,可以从一个非EDT线程中,将GUI请求添加到EDT线程的事件队列中去。
这两个方法的区别是:invokeLater是异步的,调用该方法时,该方法将GUI请求添加到事件队列中后直接返回。InvokeAndWait是同步的,调用该方法时,该方法将GUI请求添加到事件队列中后,会一直阻塞,直到该请求被完成后才会返回。
三、耗时操作应放到任务线程中,通过SwingWorker启动任务线程
EDT的事件队列的机制在保障了线程安全的同时,也引入了一个新的问题:假设事件队列中某一个GUI请求执行时间非常长,那么由于队列的特点,队列中的后续GUI请求都会被阻塞。在实际的应用程序中,表现为:点击了一个按钮触发了耗时任务后其他的组件都失去响应,必须等待该任务完成界面才能恢复响应。
我们用一个简单的程序测试一下,一个简单的swing小程序。start按钮模拟写入数据(耗时操作),display按钮用于将文本框中的内容输出到文本显示区中。数据写入完成后,在文本框中输出“数据写入完毕”。在点击start按钮后,我连续点击了三次display按钮都没有任何响应。三秒钟之后,响应结果出来了,文本显示区中输出了三行“数据写入完毕”。用户体验极差。
代码如下:
package swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TestEDT { public static void createGUI() { JFrame frame=new JFrame("swing线程机制"); JTextField tf=new JTextField("hello world"); JTextArea ta=new JTextArea(); ta.setEditable(false); JButton b1=new JButton("start"); JButton b2=new JButton("display"); JPanel p1=new JPanel(); JPanel p2=new JPanel(); p1.setLayout(new BorderLayout()); p2.add(b1); p2.add(b2); p1.add(ta,BorderLayout.CENTER); p1.add(tf,BorderLayout.NORTH); p1.add(p2,BorderLayout.SOUTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(p1); frame.pack(); frame.setVisible(true); //start按钮开始写入数据,该操作耗时很久 b1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { //用线程休眠方法模拟耗时的写入数据操作 try { Thread.sleep(3000); } catch (InterruptedException e1) { e1.printStackTrace(); } tf.setText("数据写入完毕"); } }); //display按钮用于将文本框中的信息输出到文本显示区中 b2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ta.append(tf.getText()+"\n"); } }); } //用正确的方式启动GUI public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { createGUI(); } }); } }
考虑到用户体验性,应使用独立的任务线程来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大数据量的文件等操作。
四、千万别在EDT线程中调用invokeAndWait方法
在非EDT线程中,调用invokeAndWait方法可以很好地将GUI请求添加到EDT线程的事件队列中。但是如果在EDT线程中调用该方法会发生死锁。
这是因为如果在EDT线程中调用invokeAndWait方法,GUI请求被添加到了事件队列中后,invokeAndWait方法根据其特性,会一直等待直到EDT线程执行完自己run方法中的请求为止。但是对于队列而言,默认请求是被添加到尾部的。EDT线程根据到达不了该请求的位置,因为它现在的请求也就是invokeAndWait还没有执行完。
简而言之,就是:EDT线程必须完成该方法后才能去完成该GUI请求,但是必须先完成该GUI请求才能完成该方法。这样双方互相等待,产生了死锁。
任务线程的用法
任务线程是需要显示地通过SwingWorker类调用的。
泛型参数<T,V>的分别代表:T 是 此 SwingWorker
的 doInBackground
和 get
方法返回的结果类型;V
是用于保存此 SwingWorker
的 publish
和 process
方法的中间结果的类型。
顾名思义,该类的doInBackground()方法表示在后台执行的方法,由任务线程完成,用于执行耗时操作的代码;done()方法是doInBackground方法执行完成后再调用的方法,方法体中的内容交付给EDT线程,用于处理GUI请求。
仍然以之前的模拟“写入数据”程序为例,演示任务线程的用法。
代码如下:
package swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TestEDT { public static void createGUI() { JFrame frame=new JFrame("swing线程机制"); JTextField tf=new JTextField("hello world"); JTextArea ta=new JTextArea(); ta.setEditable(false); JButton b1=new JButton("start"); JButton b2=new JButton("display"); JPanel p1=new JPanel(); JPanel p2=new JPanel(); p1.setLayout(new BorderLayout()); p2.add(b1); p2.add(b2); p1.add(ta,BorderLayout.CENTER); p1.add(tf,BorderLayout.NORTH); p1.add(p2,BorderLayout.SOUTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(p1); frame.pack(); frame.setVisible(true); //start按钮开始写入数据,该操作耗时很久 b1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { new SwingWorker<Integer,Void>(){ protected Integer doInBackground() { //模拟写入数据这一耗时操作 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return 1; } protected void done() { tf.setText("数据写入完毕"); } }.execute(); } }); //display按钮用于将文本框中的信息输出到文本显示区中 b2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ta.append(tf.getText()+"\n"); } }); } //用正确的方式启动GUI public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { createGUI(); } }); } }
测试结果为:点击start按钮后,其他的组件仍然有良好的响应。三秒钟之后,文本框内容成功显示为“数据写入完毕”。
总结一下,swing线程机制的注意事项有:
1、所有的GUI请求必须都由EDT线程完成(保障线程安全),不建议通过非EDT线程访问GUI组件
2、非EDT线程通过invokeLater和invokeAndWait方法将GUI请求交付给EDT线程。
3、禁止在EDT线程中调用invokeAndWait方法(造成死锁)。
4、耗时操作由任务线程执行,通过SwingWorker类显示启动任务线程。
over。