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