java Swing 是一个单线程图形库,里面绝大多数的代码不是线程安全的(thread-safe),Swing组件大多数没有做同步线程安全处理,也就是说任何地方都能随便调用,在不同的线程里面随便使用这些API去更新界面元素和设置值,都可能会出现一些问题。 Swing框架使用了Event Queue和EDT(Event-Dispatch-Thread)来保证线程是安全的。在GUI界面上发出的请求事件如窗口移动,刷新,按钮点击等不管是单个的还是并发的,都会背包放入事件队列(Event Queue )里面进行排队,然后事件分发线程(EDT)将会将它们一个个的取出,分派到相应的事件处理方法中。 Swing是单线程图形包就是因为处理GUI事件的事件分发线程只有一个,只要不停之GUI程序,EDT就会永不间断去处理请求。其优点是:1、将同步操作转化为异步操作;2、将并行处理转换为串行处理。 Swing编程时应该注意以下三点: 1.从其他线程访问UI组件及其事件处理器可能会导致界面更新和绘制错误。 2.在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理 3.应该使用独立的线程任务来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大数据量的文件
许多程序使用下面方法启动界面,但这是错误的启动UI界面的方法:
public class MainFrameextends 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(){ newMainFrame().setVisible(true); } }); }}当运行一个Swing程序时,会自动创建三个线程。 1.主线程,负责执行main 方法 2. toolkit 线程,负责捕捉系统事件,比如键盘、鼠标移动等,程序员不会有任何代码在这个线程上执行。Toolkit线程的作用是把自己捕获的事件传递给第三个线程,也就是事件派发线程。 3. 事件派发线程(EDT,Event Dispatcher Thread),顾名思义是用来派发事件(根据事件找到对应的事件处理代码)的线程。EDT接收来自 toolkit 线程的事件,并且将这些事件组织成一个队列,EDT的工作内容就是将这个队列中的事件按照顺序派发给相应的事件监听器,并且调用事件监听器中的回调函数,这也意味着,所有的事件处理代码都是在EDT而不是主线程中执行。 4. 上面说到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组件的刷新操作,并且派发事件队列中的下一个。
package test;import java.awt.FlowLayout;import java.awt.HeadlessException;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 SwingThreadTest3 extends JFrame { /** * */ private static final long serialVersionUID = -6785759504284102780L; 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 SwingThreadTest3() { this.setLayout(new FlowLayout()); add(progressBar); text.setEditable(flag); 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("Completed : " + String.valueOf(count) + "%"); } }; } class GoThread extends Thread { @Override public void run() { while (count < 100) { try { Thread.sleep(100); //线程沉睡 } catch (Exception e) { e.printStackTrace(); } if (flag) { count++; SwingUtilities.invokeLater(run); //将对象排到事件派发线程的队列中 } } } } private class Start implements ActionListener { public void actionPerformed(ActionEvent e) { flag = true; if(t == null) { t = new GoThread(); t.start(); } } } 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() { SwingThreadTest3 s3 = new SwingThreadTest3(); s3.setDefaultCloSEOperation(JFrame.EXIT_ON_CLOSE); s3.setSize(300, 300); s3.setVisible(true); } }); }}参考资料:http://983836259.blog.51cto.com/7311475/1724198 http://developer.51cto.com/art/201201/313034.htm
新闻热点
疑难解答