1. Swing界面假死
Swing界面编程采用事件驱动的方式,Swing线程(即事件驱动线程)负责所有组件的绘制、事件响应等等;整个界面都利用Swing线程来处理,因此不能并发执行,如对于一个按钮的事件,Swing就必须等待它处理完成才能返回继续处理其它工作,如继续响应按钮的点击、窗口的放大缩小等事件;这样如果在响应的事件中等待的时间过长,就会导致界面出现假死的现象。
举个例子:
package com.liuqi.learn.swing.thread; import java.awt.Container; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.Thread.State; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JProgressBar; import net.miginfocom.swing.MigLayout; public class FryTest extends JFrame implements ActionListener{ private JButton startButton = new JButton("Start"); public FryTest(){ this.setTitle("Fry test"); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); Container c = this.getContentPane(); c.setLayout(new MigLayout("insets 10", "[][]", "[][grow]")); this.add(startButton, "w 100!"); pack(); startButton.addActionListener(this); } public static void main(String[] args){ FryTest test = new FryTest(); test.setVisible(true); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource().equals(startButton)){ try { Thread.sleep(4000); } catch (InterruptedException e1) { e1.printStackTrace(); } } } }
每次点击按钮后,按钮状态就变成这样了:
在按钮处理它的事件的时间内(即4秒内),其它事件只能等待,四秒完成后按钮状态恢复正常。
Swing此时的状态就如同一个仅有一个人管理的餐厅:又要在前台收钱,又要在厨房炒菜;这样当有一个顾客订餐的时候,他就跑到厨房去做菜,再来顾客时就只能呆呆的在前台等着他炒完菜后回来。
那怎么解决这个问题?既然一个人太忙了,就只能再招一个人了,一人在前台收钱,一人在后台炒菜。于是就需要使用到其它线程了:
2. 引入Thread
在按钮的事件处理中,引入一个Thread来处理其事情,这样Swing线程就可以很快返回到主界面,而将耗时操作放在其它线程中在后台执行。修改后的事件处理如下所示:
@Override public void actionPerformed(ActionEvent e) { if (e.getSource().equals(startButton)){ new Thread(){ public void run(){ try { Thread.sleep(4000); } catch (InterruptedException e1) { e1.printStackTrace(); } } }.start(); } }
这次点击按钮,按钮再也不会像刚刚那样一直要四秒后才能恢复正常了,而是立即正常。
这里就使用到了事件处理线程之外的线程来完成具体的工作;但需要注意的是,当事件处理中需要对界面进行更新时,就需要使用另外的方式来使用多线程了。Java相关文档中指出,当组件被显示到界面上之后,就应该只有事件处理线程对它的外观进行改变,否则随时可能引起死锁!
因为使用的这些方法有很多是非线程安全的,同时使用工作线程和事件处理线程来进行处理,就有可能引起死锁。
当然也有例外,有些方法 如repaint、JTextComponent的setText方法等,是线程安全的,可以有多个线程对其进行访问。但在不确定的时候,最好是使用invokeAndWait或者是invokeLater来进行处理。
3. invokeAndWait与invokeLater
invokeAndWait会将一直等到事件处理返回,而invokeLater会将一个事件加入到队列后立即返回,而不管这个事件在什么时候进行处理。
这两个方法都会使得执行事件中的处理异步的在事件处理线程中进行,因此可以安全地对界面组件进行控制而不会引起死锁。
但需要注意的是如果从事件处理线程中调用invokeAndWait,也会引起事件处理线程一直在等待而出现界面死锁的现象。
但代码中过多的invokeAndWait及invokeLater会使得对代码的控制变得困难;于是新的方法又被引入,即SwingWorker
4. SwingWorker顾名思义,即是一个Worker,它主要负责将一些繁复的工作放到后台去执行,而在这个执行的过程中又可以将中间结果以一种安全的方式在界面上显示出来,从而不会引起死锁。用它来进行编程的框架如下所示 :
public class LoadWorker extends SwingWorker{ public LoadWorker(ContentTextPane textPane, File file){ } @Override protected Void doInBackground() throws Exception { publish("test"); return null; } protected void process(List list){ } @Override protected void done(){ } }
主要有四个方法:
doInBackGround:在后台执行的耗时操作,其返回结果类型是SwingWorker<..,...>中的第一个参数类型
publish(...):在doInBackGround工作中将中间结果发送给Process进行处理
process(List<...> list):接受publish发送过来的数据,并更新界面显示。
done():线程完成时所做的工作
注意在doInBackground方法中执行的工作,是在非事件处理线程中执行的,因此不能在其中更新界面组件;如果需要更新界面组件,则可以使用publish返回数据,然后在process中进行处理——process中的操作是在事件处理线程完成的,因此可以安全的访问控制组件。
一个例子:
package com.liuqi.logtools.util; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.util.Calendar; import java.util.List; import javax.swing.SwingWorker; import javax.swing.text.BadLocationException; import com.liuqi.logtools.ui.ContentTextPane; public class CopyOfLoadWorker extends SwingWorker{ private ContentTextPane textPane; private File file; private FilterCache cache; private Calendar startCalendar; public static String WORK_THREAD_END = "WORKTHREADEND"; private String debugStr = LogLevel.DEBUG.toString(); private String errorStr = LogLevel.ERROR.toString(); private String warnStr = LogLevel.WARN.toString(); private String infoStr = LogLevel.INFO.toString(); public CopyOfLoadWorker(ContentTextPane textPane, File file) { this.textPane = textPane; this.file = file; try { textPane.getDocument() .remove(0, textPane.getDocument().getLength()); } catch (BadLocationException e) { e.printStackTrace(); } cache = textPane.getFilterCache(); } @Override protected Void doInBackground() throws Exception { startCalendar = Calendar.getInstance(); // 先清空显示 BufferedReader reader = null; reader = new BufferedReader(new FileReader(file)); String line = ""; while ((line = reader.readLine()) != null) { if (!cache.isSatisfy(line)) { continue; } publish(line); } reader.close(); return null; } protected void process(List list) { for (String line : list) { String str = ""; if (line.length() > 130) { str = line.substring(0, 130); } else if (line.indexOf(0) == '[') { str = line.substring(0, 8); } else { str = line; } if (str.contains(debugStr)) { textPane.debug(line); } else if (str.contains(infoStr)) { textPane.info(line); } else if (str.contains(errorStr)) { textPane.error(line); } else { textPane.warn(line); } } } @Override protected void done() { if (null != startCalendar) { System.out.println("Total time: " + (Calendar.getInstance().getTimeInMillis() - startCalendar .getTimeInMillis())); } } }
在该例子中,由doInBackground方法中读取一个文件,对其中的行进行判断其是否满足一定的条件,然后再使用publish传递到process方法中,由process来将其显示在textPane上。在工作完成之后,计算其所消耗的时间。