Java Swing中的多线程

引言

Java使用Swing后就会有三个线程

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

所以一个是程序启动时就产生的线程,执行main()方法,一个是Swing的事件派发线程(EDT),所以只要使用Swing就设计到多线程操作。

问题

Swing如果只是处理简单操作。这几个线程就可以使用,但如果Swing中需要进行长时间操作,只使用这两个线程就会导致程序出错,如下面这个程序

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.JPanel;
import javax.swing.JTextField;


public class MySwing extends JFrame {
	//定义属性
	private JFrame fr = new JFrame();  
	private JPanel jp = new JPanel(); //面板
	private JTextField jt = new JTextField(10);
	private JButton jb = new JButton("Start");
	
	
	/**
	 * 按钮的监听函数
	 */
	private void JbListener(){
		jb.addActionListener(new ActionListener() { 
	         public void actionPerformed(ActionEvent e) {
	             try{
	            	 jt.setText("1.检查数据合法性...");
	                Thread.sleep(3000);//模仿检测数据合法性
	                jt.setText("2.正在导入数据...");
	                Thread.sleep(4000);//模仿导入数据
	                jt.setText("3.导入成功!");
	             }catch (InterruptedException e1) {
	                e1.printStackTrace();
	             }
	         }
	     });
	}
	

	/**
	 * 初始化Swing
	 */
	public void Demo(){ 
		jp.add(jt);
		jp.add(jb);
		jp.setLayout(new FlowLayout());
		fr.add(jp);
		JbListener();
		fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		fr.setSize(300, 100);
		fr.setVisible(true);
		
	}
	
	
	public static void main(String[] args) {
		
		new MySwing().Demo();
		
	}
}

击按钮,界面卡住,按钮变得不可触发,直到一段时间(7秒)之后界面显示“3.导入成功”。期间并没有显示“1.检查数据合法性”和“2.正在导入数据。
Java Swing中的多线程_第1张图片

原因

出现上面这种情况的原因在于importBtn事件是由UI线程响应的,Thread.sleep()阻塞了UI线程,UI线程不会执行任何界面刷新的操作。
什么,你说你还不懂?
在引言中提到EDT线程是按顺序触发的,只有前一个任务完成,它才会执行下一个任务,在上面的程序里执行按钮任务时,由于UI界面更新又新添加了3个任务,但这三个任务不会执行,直到按钮任务完成,逻辑图如下:
Java Swing中的多线程_第2张图片
如上图所示,在按钮事件处理完之后。EDT才会处理队列中的其他操作,对于界面的更改操作最后是连续执行的,但每一步都很快,所以只能看到最后的结果,即,导入更新。

invokeLater和invokeAndWait

那么问题来了,怎么改呢?这就得将耗时操作放在其他线程中执行,在其他线程中更新界面的话就通过invokeLater和invokeAndWait这两个方法。
SwingUtilities的invokeLater和invokeAndWait方法可以将一个可执行对象(Runnable)实例追加到EDT的可执行队列中。
那么上面的程序这么改就可以了

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.JPanel;
import javax.swing.JTextField;


public class MySwing extends JFrame {
	//定义属性
	private JFrame fr = new JFrame();  
	private JPanel jp = new JPanel(); //面板
	private JTextField jt = new JTextField(10);
	private JButton jb = new JButton("Start");
	
	
	/**
	 * 按钮的监听函数
	 */
	private void JbListener(){
		jb.addActionListener(new ActionListener() { 
	         public void actionPerformed(ActionEvent e) {
	        	 //定义匿名内部类,该类实现Runnable接口
	        	new Thread(new Runnable() {
					public void run() {
						try{
			            	jt.setText("1.检查数据合法性...");
			                Thread.sleep(3000);//模仿检测数据合法性
			                jt.setText("2.正在导入数据...");
			                Thread.sleep(4000);//模仿导入数据
			                jt.setText("3.导入成功!");
			             }catch (InterruptedException e1) {
			                e1.printStackTrace();
			             }
					}
				}).start();
	         }
	     });
	}
	

	/**
	 * 初始化Swing
	 */
	public void Demo(){ 
		jp.add(jt);
		jp.add(jb);
		jp.setLayout(new FlowLayout());
		fr.add(jp);
		JbListener();
		fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		fr.setSize(300, 100);
		fr.setVisible(true);
		
	}
	
	
	public static void main(String[] args) {
		
		new MySwing().Demo();
		
	}
}

重新创建一个线程,在这个新线程中执行所有操作,包括延时及界面更新,这样可以显示我们想要的结果。
但是,不能在其他线程中操作EDT线程,会导致界面不稳定
关于如何不稳定,我没遇到过,其实在开始Swing编程中,大部分人都会有这个不良和的编程习惯,即在其他线程中操作EDT线程。最常见的就是在主程序中直接显示界面,是的,只要你没用invokeLater和invokeAndWait这两个方法,你就肯定犯过这个错误,因为从主线程跳到EDT线程显示界面也需要这两个函数。
重新更改后的程序如下:

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.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;


public class MySwing extends JFrame {
	//定义属性
	private JFrame fr = new JFrame();  
	private JPanel jp = new JPanel(); //面板
	private JTextField jt = new JTextField(10);
	private JButton jb = new JButton("Start");
	
	
	/**
	 * 按钮的监听函数
	 */
	private void JbListener(){
		jb.addActionListener(new ActionListener() { 
	         public void actionPerformed(ActionEvent e) {
	        	 //定义匿名内部类,该类实现Runnable接口
	        	new Thread(new Runnable() {
					public void run() {
						try{
							SwingUtilities.invokeLater(new Runnable() {
								public void run() {
									jt.setText("1.检查数据合法性...");
								}
							});
							
			                Thread.sleep(3000);//模仿检测数据合法性
			                SwingUtilities.invokeLater(new Runnable() {
								public void run() {
									jt.setText("2.正在导入数据...");
								}
							});
			                Thread.sleep(4000);//模仿导入数据
			                SwingUtilities.invokeLater(new Runnable() {
								public void run() {
									 jt.setText("3.导入成功!");
								}
							});             
			             }catch (InterruptedException e1) {
			                e1.printStackTrace();
			             }
					}
				}).start();
	         }
	     });
	}
	

	/**
	 * 初始化Swing
	 */
	public void Demo(){ 
		jp.add(jt);
		jp.add(jb);
		jp.setLayout(new FlowLayout());
		fr.add(jp);
		JbListener();
		fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		fr.setSize(300, 100);
		fr.setVisible(true);
		
	}
	
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				new MySwing().Demo();
			}
		});
		
		
	}
}

关于invokeLater和invokeAndWait这两个方法的区别,invokeLater在把可运行的对象放入队列后就返回,而invokeAndWait一直等待知道已启动了可运行的run方法才返回。

计时器程序

最后用上面学到的知识编写一个计时器程序
Java Swing中的多线程_第3张图片
单击start按钮开始计时,单击stop按钮停止计时
程序如下

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.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class MySwing2 extends JFrame {
	//定义属性
	private JFrame frame = new JFrame();  
	private JPanel panel = new JPanel(); //面板
	private JTextField text = new JTextField(10);
	private JButton start = new JButton("Start");
	private JButton stop = new JButton("stop");
	private int count = 0;
	private Boolean Flag=false;
	
	private void AppearanceSetting(){
		frame.setSize(300, 100);
		frame.setLayout(new FlowLayout());
	}
	
	
	private void FrameSetting(){
		text.setEditable(false);
		panel.add(text);
		panel.add(start);
		panel.add(stop);
		panel.setLayout(new FlowLayout());
		frame.add(panel);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);
	}


	private void Jb1Listener(){
		//start.addActionListener(new Start());
		start.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {
				
				Flag=true;
				
				new Thread(new Runnable(){
					@Override
					public void run() {
						while(true){
							
							if(Flag){
								try {
									Thread.sleep(1000);
								} catch (InterruptedException e) {
									e.printStackTrace();
								}
								count++;
								SwingUtilities.invokeLater(new Runnable(){
									public void run() {
										// TODO Auto-generated method stub
										text.setText(String.valueOf(count));
									}	
								});	
							}
						}

					}
				}).start();
			}
		});
	}

	
	private void Jb2Listener(){
		stop.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				Flag=false;
			}
		});
	}
	
	
	
	public void Demo(){
		AppearanceSetting();
		FrameSetting();
		Jb1Listener();
		Jb2Listener();
	}
	
	public static void main(String[] args) {
		
		SwingUtilities.invokeLater(new Runnable(){
			public void run() {
				new MySwing2().Demo();
			}

		});

	}
}

后记

这次多线程的文章前前后后拖了一周快写完,对EDT的理解比在写文章之前更清楚了,多写写还是有好处的,下次更新应该是MySQL相关内容。

你可能感兴趣的:(Java Swing中的多线程)