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