多線程和swing

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章