java swing里面大部分类都不是线程安全的,如果通过多个线程去操作swing对象,很可能会出现很多诡异的现象,如果你想让它变成线程安全的,就需要用一个特殊的线程去操作swing对象,也就是EDT线程,也就是事件调度线程(Event Dispatch Thread,EDT)
一个GUI 程序有很多线程同时运行,其中有一个叫做 事件调度线程(EDT),用来处理我们在程序里面所有的回调(最常见的就是我们点击按钮后执行的actionPerformed方法),所有的操作Swing对象的操作必须放在这个线程里面,否则就会出问题
看一个例子
import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JTextField; import javax.swing.SwingUtilities; /** * <code>NotInEDTSample</code> just demonstrates the usage of Swing EDT simply. * * @author Jimmy.haung(SZ Team) * @since <i>DUI (Mar 25, 2013)</i> */ public class NotInEDTSample extends JFrame { private static final long serialVersionUID = 1L; private JTextField m_txt; public NotInEDTSample() { initGUI(); notInEDT(); } /** * Init the GUI */ public void initGUI() { this.setTitle("a simple EDT Sample"); m_txt = new JTextField(); getContentPane().add(m_txt, BorderLayout.CENTER); } /** * Process not under the EDT. 这里我启动了10个线程来改变<code>m_txt</code>的内容. */ private void notInEDT() { for (int i = 0; i < 4; ++i) { new Thread(new Runnable() { @Override public void run() { while (true) { m_txt.setText("我不在EDT中操作!"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } } /*private void notInEDT() { for (int i = 0; i < 4; ++i) { new Thread(new Runnable() { @Override public void run() { while (true) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { m_txt.setText("我在EDT中操作!"); } }); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }*/ /** * Launch the application. */ public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { try { NotInEDTSample oFrame = new NotInEDTSample(); oFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); oFrame.setLocationRelativeTo(null); oFrame.setSize(300, 200); oFrame.setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } }
我们在notInEDT用其他线程去改变Text的值,应该很快就会出问题,而且没有抛出异常
只要我们把下面的这个样子,让EDT线程去处理就没有问题了
private void notInEDT() { for (int i = 0; i < 4; ++i) { new Thread(new Runnable() { @Override public void run() { while (true) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { m_txt.setText("我在EDT中操作!"); } }); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
这里调用了invokeLater方法把UI操作丢给EDT线程去处理,看看说明
让一个Runnable接口的run方法能够异步在EDT线程里面执行,等于把这个操作假如到一个队列队尾,等前面的操作都完成了再执行这个操作
/** * Causes <i>doRun.run()</i> to be executed asynchronously on the * AWT event dispatching thread. This will happen after all * pending AWT events have been processed. This method should * be used when an application thread needs to update the GUI. * In the following example the <code>invokeLater</code> call queues * the <code>Runnable</code> object <code>doHelloWorld</code> * on the event dispatching thread and * then prints a message. * <pre> * Runnable doHelloWorld = new Runnable() { * public void run() { * System.out.println("Hello World on " + Thread.currentThread()); * } * }; * * SwingUtilities.invokeLater(doHelloWorld); * System.out.println("This might well be displayed before the other message."); * </pre> * If invokeLater is called from the event dispatching thread -- * for example, from a JButton's ActionListener -- the <i>doRun.run()</i> will * still be deferred until all pending events have been processed. * Note that if the <i>doRun.run()</i> throws an uncaught exception * the event dispatching thread will unwind (not the current thread). * <p> * Additional documentation and examples for this method can be * found in * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How to Use Threads</a>, * in <em>The Java Tutorial</em>. * <p> * As of 1.3 this method is just a cover for <code>java.awt.EventQueue.invokeLater()</code>. * <p> * Unlike the rest of Swing, this method can be invoked from any thread. * * @see #invokeAndWait */ public static void invokeLater(Runnable doRun)
有一个可以功能类似但是有区别的方法,invokeAndWait方法,功能和invokeLater差不多,也是可以把run方法放到EDT里面执行,但是区别在于是同步的,并且不能在EDT里面被调用
/** * Causes <code>doRun.run()</code> to be executed synchronously on the * AWT event dispatching thread. This call blocks until * all pending AWT events have been processed and (then) * <code>doRun.run()</code> returns. This method should * be used when an application thread needs to update the GUI. * It shouldn't be called from the event dispatching thread. * Here's an example that creates a new application thread * that uses <code>invokeAndWait</code> to print a string from the event * dispatching thread and then, when that's finished, print * a string from the application thread. * <pre> * final Runnable doHelloWorld = new Runnable() { * public void run() { * System.out.println("Hello World on " + Thread.currentThread()); * } * }; * * Thread appThread = new Thread() { * public void run() { * try { * SwingUtilities.invokeAndWait(doHelloWorld); * } * catch (Exception e) { * e.printStackTrace(); * } * System.out.println("Finished on " + Thread.currentThread()); * } * }; * appThread.start(); * </pre> * Note that if the <code>Runnable.run</code> method throws an * uncaught exception * (on the event dispatching thread) it's caught and rethrown, as * an <code>InvocationTargetException</code>, on the caller's thread. * <p> * Additional documentation and examples for this method can be * found in * <A HREF="http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html">How to Use Threads</a>, * in <em>The Java Tutorial</em>. * <p> * As of 1.3 this method is just a cover for * <code>java.awt.EventQueue.invokeAndWait()</code>. * * @exception InterruptedException if we're interrupted while waiting for * the event dispatching thread to finish excecuting * <code>doRun.run()</code> * @exception InvocationTargetException if an exception is thrown * while running <code>doRun</code> * * @see #invokeLater */ public static void invokeAndWait(final Runnable doRun) throws InterruptedException, InvocationTargetException
下面我来系统分析一下为什么不能在EDT里面调用 invokeAndWait
首先需要理解java swing的event 队列,我们对UI的基本所有操作都会生成一个event,加入到event队列里面,由EDT线程来逐一处理,比如我们鼠标点击一个按钮,就是把注册在按钮里面的事件加入event里面去处理
我们来比较invokeLater和invokeAndWait的源码区别
/** * invokeLater将我们的runnable包装成事件丢进eventQueue就没有管了 */ public static void invokeLater(Runnable runnable) { Toolkit.getEventQueue().postEvent( new InvocationEvent(Toolkit.getDefaultToolkit(), runnable)); }
static void invokeAndWait(Object source, Runnable runnable) throws InterruptedException, InvocationTargetException { if (EventQueue.isDispatchThread()) {//这里是防止在EDT里面调用 throw new Error("Cannot call invokeAndWait from the event dispatcher thread"); } class AWTInvocationLock {} Object lock = new AWTInvocationLock(); InvocationEvent event = new InvocationEvent(source, runnable, lock, true); //这里是关键,当线程进入这里之后获得了锁,然后wait了 synchronized (lock) { Toolkit.getEventQueue().postEvent(event); lock.wait(); } Throwable eventThrowable = event.getThrowable(); if (eventThrowable != null) { throw new InvocationTargetException(eventThrowable); } }
invokeAndWait的时候wait了,然后看在是什么地方notify的,InvocationEvent里面
/** * Executes the Runnable's <code>run()</code> method and notifies the * notifier (if any) when <code>run()</code> has returned or thrown an exception. * * @see #isDispatched */ public void dispatch() { try { if (catchExceptions) { try { runnable.run(); } catch (Throwable t) { if (t instanceof Exception) { exception = (Exception) t; } throwable = t; } } else { runnable.run(); } } finally { dispatched = true; if (notifier != null) { synchronized (notifier) { notifier.notifyAll();//执行完我们的操作后,notify所有线程 } } } }
这就是为什么说invokeAndWait这个方法是同步的原因,调用这个方法的线程会一直堵塞知道执行完毕
现在假如我们在EDT里面调用invokeAndWait,务必会造成死锁,看看下一面这个例子
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JTextField; import javax.swing.SwingUtilities; public class TestAction extends JFrame { private static final long serialVersionUID = -7462155330900531124L; private JButton jb1 = new JButton("确定"); private JTextField txt = new JTextField(10); public TestAction() { jb1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String name = ((JButton) e.getSource()).getText(); txt.setText(name); } }); setLayout(null); add(txt); add(jb1); txt.setBounds(50, 100, 200, 30); jb1.setBounds(270, 100, 70, 30); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { SwingConsole.run(new TestAction(), 500, 500); } }); } }
import javax.swing.*; public class SwingConsole { public static void run(final JFrame f, final int width, final int height) { SwingUtilities.invokeLater(new Runnable() { public void run() { f.setTitle(f.getClass().getSimpleName()); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setSize(width, height); f.setVisible(true); } }); } }
这样写是没有错误的~~假如我们用invokeAndWait,肯定就死锁了
import java.lang.reflect.InvocationTargetException; import javax.swing.*; public class SwingConsole { public static void run(final JFrame f, final int width, final int height){ try { SwingUtilities.invokeAndWait(new Runnable(){ public void run(){ f.setTitle(f.getClass().getSimpleName()); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setSize(width, height); f.setVisible(true); } }); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }
这部分还有一个重点,不要在EDT里面调用费时间的操作,这样会造成界面卡主
来看一个例子,我们模拟10个文件有序上传,并且显示进度条
import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; public class TestProgress extends Thread implements ActionListener { private static JProgressBar progressBar; JFrame jf = new JFrame("Test"); JPanel jp = new JPanel(); JTextArea jta = new JTextArea(); JButton jb = new JButton("点击"); public static void main(String[] args) { new TestProgress(); } public TestProgress() { jp.setLayout(new FlowLayout()); progressBar = new JProgressBar(); progressBar.setValue(0); progressBar.setStringPainted(true); jf.add(jp, BorderLayout.NORTH); jf.add(new JScrollPane(jta)); jp.add(progressBar); jp.add(jb); jf.add(new JScrollPane(jta), BorderLayout.CENTER); jb.addActionListener(this); jf.setSize(300, 200); jf.setLocation(300, 200); jf.setVisible(true); jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } @Override public void run() { for (int i = 0; i < 10;i++) {//10个文件 UpLordTread lordTread = new UpLordTread(progressBar,jta,"文件" + i); lordTread.start();//启动上传程序 try { lordTread.join();//这就是关键~~等待这个线程完成 } catch (InterruptedException e) { e.printStackTrace(); } } } public void actionPerformed(ActionEvent e) { String comm = e.getActionCommand(); if ("点击".equals(comm)) { this.start();//不能在EDT线程里面执行费时的操作,防止UI卡死 jb.setEnabled(false); } } } /** * 文件上传线程 * @author yellowbaby * */ class UpLordTread extends Thread{ JTextArea jta; JProgressBar progressBar; public UpLordTread(JProgressBar progressBar,JTextArea jta,String fileName) { super(fileName); this.jta = jta; this.progressBar = progressBar; } public void run() { for (int i = 0; i <= 100; i++) { progressBar.setValue(i); String temp = Thread.currentThread().getName() + ":" + i + "\n"; jta.append(temp); try { Thread.sleep(10); } catch (Exception ee) { ee.printStackTrace(); } } progressBar.setValue(0); } }
我们点击按钮后,我并没有,在actionPerformaed里面直接调用上传的循环代码,而是从新开了一个线程,去执行,这是为什么呢?
因为actionPerformaed的代码是由EDT调用的,如果这个方法不立即返回的话,EDT线程就无法去处理其他事件,界面也就卡死了
可以试试把代码改成这样,点击按钮后界面就会卡死,然后就只有等到全部上传后界面才恢复
import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; public class TestProgress extends Thread implements ActionListener { private static JProgressBar progressBar; JFrame jf = new JFrame("Test"); JPanel jp = new JPanel(); JTextArea jta = new JTextArea(); JButton jb = new JButton("点击"); public static void main(String[] args) { new TestProgress(); } public TestProgress() { jp.setLayout(new FlowLayout()); progressBar = new JProgressBar(); progressBar.setValue(0); progressBar.setStringPainted(true); jf.add(jp, BorderLayout.NORTH); jf.add(new JScrollPane(jta)); jp.add(progressBar); jp.add(jb); jf.add(new JScrollPane(jta), BorderLayout.CENTER); jb.addActionListener(this); jf.setSize(300, 200); jf.setLocation(300, 200); jf.setVisible(true); jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } @Override public void run() { for (int i = 0; i < 10;i++) {//10个文件 UpLordTread lordTread = new UpLordTread(progressBar,jta,"文件" + i); lordTread.start();//启动上传程序 try { lordTread.join();//这就是关键~~等待这个线程完成 } catch (InterruptedException e) { e.printStackTrace(); } } } public void actionPerformed(ActionEvent e) { String comm = e.getActionCommand(); if ("点击".equals(comm)) { //this.start();//不能在EDT线程里面执行费时的操作,防止UI卡死 for (int i = 0; i < 10;i++) {//10个文件 UpLordTread lordTread = new UpLordTread(progressBar,jta,"文件" + i); lordTread.start();//启动上传程序 try { lordTread.join();//这就是关键~~等待这个线程完成 } catch (InterruptedException e1) { e1.printStackTrace(); } } jb.setEnabled(false); } } } /** * 文件上传线程 * @author yellowbaby * */ class UpLordTread extends Thread{ JTextArea jta; JProgressBar progressBar; public UpLordTread(JProgressBar progressBar,JTextArea jta,String fileName) { super(fileName); this.jta = jta; this.progressBar = progressBar; } public void run() { for (int i = 0; i <= 100; i++) { progressBar.setValue(i); String temp = Thread.currentThread().getName() + ":" + i + "\n"; jta.append(temp); try { Thread.sleep(10); } catch (Exception ee) { ee.printStackTrace(); } } progressBar.setValue(0); } }
所以我们的费时操作都要丢到背后线程去处理,JDK里面有一个SwingWork可以帮我们处理这个问题
import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; public class TestProgress extends Thread implements ActionListener { private static JProgressBar progressBar; JFrame jf = new JFrame("Test"); JPanel jp = new JPanel(); JTextArea jta = new JTextArea(); JButton jb = new JButton("点击"); public static void main(String[] args) { new TestProgress(); } public TestProgress() { jp.setLayout(new FlowLayout()); progressBar = new JProgressBar(); progressBar.setValue(0); progressBar.setStringPainted(true); jf.add(jp, BorderLayout.NORTH); jf.add(new JScrollPane(jta)); jp.add(progressBar); jp.add(jb); jf.add(new JScrollPane(jta), BorderLayout.CENTER); jb.addActionListener(this); jf.setSize(300, 200); jf.setLocation(300, 200); jf.setVisible(true); jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } public void actionPerformed(ActionEvent e) { String comm = e.getActionCommand(); if ("点击".equals(comm)) { SwingWorker<Void, Void> swingWorker = new SwingWorker<Void, Void>() { @Override protected Void doInBackground() throws Exception { for (int i = 0; i < 10; i++) {// 10个文件 UpLordTread lordTread = new UpLordTread(progressBar, jta, "文件" + i); lordTread.start();// 启动上传程序 try { lordTread.join();// 这就是关键~~等待这个线程完成 } catch (InterruptedException e) { e.printStackTrace(); } } return null; } @Override protected void done() { System.out.println("上传成功"); } }; swingWorker.execute(); jb.setEnabled(false); } } } /** * 文件上传线程 * * @author yellowbaby * */ class UpLordTread extends Thread { JTextArea jta; JProgressBar progressBar; public UpLordTread(JProgressBar progressBar, JTextArea jta, String fileName) { super(fileName); this.jta = jta; this.progressBar = progressBar; } public void run() { for (int i = 0; i <= 100; i++) { progressBar.setValue(i); String temp = Thread.currentThread().getName() + ":" + i + "\n"; jta.append(temp); try { Thread.sleep(10); } catch (Exception ee) { ee.printStackTrace(); } } progressBar.setValue(0); } }
SwingWork里面有两个主要的方法,doInBackground和done,doInBackground就是我们的背后线程,done是做完doInBackground后调用的,主要用来更新UI