Java中事件分发线程(EDT)与SwingUtilities.invokeLater相关总结

前言:这篇文章严格来说不算原创,算是我对这方面知识的一点小结,素材来至其他网友。当然我在我写的C段查询工具也用到了这方面的东西,不过由于代码太多不方便用作事例,因此用了他人的素材总结一下,望理解O(∩_∩)O~

一 Swing线程基础

一个Swing程序中一般有下面三种类型的线程:
    * 初始化线程(Initial Thread)
    * UI事件调度线程(EDT)
    * 任务线程(Worker Thread)
每个程序必须有一个main方法,这是程序的入口。该方法运行在初始化或启动线程上。初始化线程读取程序参数并初始化一些对象。在许多Swing程序中,该线程主要目的是启动程序的图形用户界面(GUI)。一旦GUI启动后,对于大多数事件驱动的桌面程序来说,初始化线程的工作就结束了。

Swing程序只有一个用EDT,该线程负责GUI组件的绘制和更新,通过调用程序的事件处理器来响应用户交互。所有事件处理都是在EDT上进行的,程序同UI组件和其基本数据模型的交互只允许在EDT上进行,所有运行在EDT上的任务应该尽快完成,以便UI能及时响应用户输入。

Swing编程时应该注意以下三点:
1.从其他线程访问UI组件及其事件处理器会导致界面更新和绘制错误。
2.在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理。
3.应使用独立的任务线程来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大树据量的文件。
总之,任何干扰或延迟UI事件的处理只应该出现在独立任务线程中;在初始化线程或任务线程同Swing组件或其缺省数据模型进行的交互都是非线程安全性操作。


二 使用合适线程

初始化线程运行程序的main方法,该方法能处理许多任务。但在典型的Swing程序中,其主要任务就是创建和运行应用程序的界面。创建UI的点,也就是程序开始将控制权转交给UI时的点,往往是同EDT交互出现问题的第一个地方。

许多程序使用下面方法启动界面,但这是错误的启动UI界面的方法:
public class MainFrame
extends javax.swing.JFrame {
 …


 public static void main(String[] args)
{
   new MainFrame().setVisible(true);
 }
}

尽管这种错误出现在开始,但仍然违反了不应在EDT外的其他线程同Swing组件交互的原则。这个错误尤其容易犯,线程同步问题虽然不是马上显示出来,但是还要注意避免这样书写。

正确启动UI界面应该如下:

public class MainFrame extends javax.swing.JFrame
{
 …

 public static void main(String[] args)
{
   SwingUtilities.invokeLater(new Runnable()
{
     public void run()
{
       new
MainFrame().setVisible(true);
     }
   });
 }
}

使用NetBeans IDE的开发者应该对这段代码很熟悉,NetBeans通常会自动生成这段代码。这段启动代码虽然和SwingWorker没有直接关系,但是这个编程范式很重要。SwingUtilities类包含一些静态方法帮你同UI组件交互,其中invokeLater方法意思是在EDT上执行其Runnable任务。Runnable接口定义了可作为独立线程执行的任务。

在初始化线程中使用invokeLater方法能正确的初始化程序界面。就像前面文章所提到的,此方法是异步执行的,也就是说调用会立即返回。创建界面后,大部分初始化线程基本上就结束了。

通常有两种办法调用此方法:
* SwingUtilities.invokeLater
* EventQueue.invokeLater
两个方法都是正确的,选择任何一个都可以。实际上,SwingUtilities版只是一个薄薄的封装方法,它直接转而调用EventQueue.invokeLater。因为Swing框架本身经常调用SwingUtilities,使用SwingUtilities可以减少程序引入的类。

另种将任务放到EDT执行的方法是SwingUtilities.invokeAndWait,不像invokeLater,invokeAndWait方法是阻塞执行的,它在EDT上执行Runnnable任务,直到任务执行完了,该方法才返回调用线程。

invokeLater和invokeAndWait都在事件派发队列中的所有事件都处理完之后才执行它们的Runnable任务,也就是说,这两个方法将Runnable任务放在事件队列的末尾。

注意:虽然可以在其他线程上调用invokeLater,也可以在EDT上调用invokeLater,但是千万不要在EDT线程上调用invokeAndWait方法!很容易理解,这样做会造成线程竞争,程序就会陷入死锁。


三 进一步理解事件派发线程(EDT)

当运行一个 Swing 程序时,会自动创建三个线程。
1.主线程,负责执行main 方法。
2. toolkit 线程,负责捕捉系统事件,比如键盘、鼠标移动等,程序员不会有任何代码在这个线程上执行。Toolkit线程的作用是把自己捕获的事件传递给第三个线程,也就是事件派发线程。
3. 事件派发线程(EDT,Event Dispatcher Thread),顾名思义是用来派发事件(根据事件找到对应的事件处理代码)的线程。EDT接收来自 toolkit 线程的事件,并且将这些事件组织成一个队列,EDT的工作内容就是将这个队列中的事件按照顺序派发给相应的事件监听器,并且调用事件监听器中的回调函数,这也意味着,所有的事件处理代码都是在EDT而不是主线程中执行。
上面说到EDT中维护了一个事件的队列,并且它们是按照顺序派发的。由于事件派发是单线程的操作,所以只有等待前面事件监听器的回调函数执行完毕,才能够执行组件更新的操作,以及继续派发后面的事件。这样导致的一个后果就是:当在一个事件监听回调函数中做了耗时的操作,那么,界面会因此停住,并且界面上所有控件失效(不可触发)。
解决这个问题的方法是:在事件处理函数中将耗时的操作放到新线程(一般称之为工作线程)中执行,而不是让其在EDT中执行。

案例:

一个窗口,有一个按钮和一个label。点击按钮,系统将做模仿导入数据的动作,导入数据之前需要检测数据的合法性。并且,检测数据和导入数据这两个步骤都需要耗费一定的时间。
如果没有之前说到的EDT的概念,那么你可能会这么做:

importBtn.addActionListener(newActionListener() {
           @Override
            publicvoid actionPerformed(ActionEvent e) {
                try{
                   lb.setText("1.检查数据合法性...");
                   Thread.sleep(3000);//模仿检测数据合法性
                   lb.setText("2.正在导入数据...");
                   Thread.sleep(4000);//模仿导入数据
                   lb.setText("3.导入成功!");
                }catch (InterruptedException e1) {
                   e1.printStackTrace();
                }
            }
        });

但是,如果运行一下的话,会发现现象是这样:点击按钮,界面卡住,按钮变得不可触发,直到一段时间(7秒)之后界面显示“3.导入成功”。期间并没有显示“1.检查数据合法性”和“2.正在导入数据”。
这个现象印证了上面说的理论:当事件派发线程中正在执行的事件监听函数执行完毕,才能进行UI组件的刷新操作,并且派发事件队列中的下一个。


四 使用SwingUtilities.invokeLater实现的一个进度条实例

运行后截图如下:

wKioL1Zw2ZbR7kqPAAA-wU5QcBw100.png

完整代码:

package test;

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class SwingThreadTest2 extends JFrame {
	private static final long serialVersionUID = 1L;
	private static final String STR = "Completed : ";
	private JProgressBar progressBar = new JProgressBar();
	private JTextField text = new JTextField(10);
	private JButton start = new JButton("Start");
	private JButton end = new JButton("End");
	private boolean flag = false;
	private int count = 0;

	private GoThread t = null;

	private Runnable run = null;// 更新组件的线程

	public SwingThreadTest2() {
		this.setLayout(new FlowLayout());
		add(progressBar);
		text.setEditable(false);
		add(text);
		add(start);
		add(end);
		start.addActionListener(new Start());
		end.addActionListener(new End());

		run = new Runnable() {// 实例化更新组件的线程
			public void run() {
				progressBar.setValue(count);
				text.setText(STR + String.valueOf(count) + "%");
			}
		};
	}

	private class Start implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			flag = true;
			if (t == null) {
				t = new GoThread();
				t.start();
			}
		}
	}

	class GoThread extends Thread {
		public void run() {
			while (count < 100) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				if (flag) {
					count++;
					SwingUtilities.invokeLater(run);// 将对象排到事件派发线程的队列中
				}
			}
		}
	}

	private class End implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			flag = false;
		}
	}

	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				SwingThreadTest2 fg = new SwingThreadTest2();
				fg.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				fg.setSize(300, 100);
				fg.setVisible(true);
				
			}
		});
		
	}
}

附:参考文档:

1 使用SwingWorker之一  http://blog.sina.com.cn/s/blog_4b6047bc010007so.html

2 SwingUtilities的invokeLater和invokeAndWait  http://blog.csdn.net/yanwushu/article/details/39434159

3 [转]方法SwingUtilities.invokeLater()的作用  http://blog.csdn.net/hsnxyc/article/details/6501284


你可能感兴趣的:(java,swing,SwingUtilities,事件分发线程(EDT))