在介紹swing線程機制以前,先介紹一些背景概念。java
背景概念數據庫
同步與異步:小程序
同步是指程序在發起請求後開始處理事件並等待處理的結果或等待請求執行完畢,在此以前程序被阻塞(block)直到請求完成。安全
異步是當前程序發起請求後當即返回,當前程序不會當即處理該事件並等待處理的結果,請求是在稍後的某一時間才被處理。數據結構
串行與並行:併發
串行是指多個要處理的請求按照順序執行,處理完一個再處理下一個。app
並行能夠理解爲併發,指的是同時處理多個請求(實際上咱們只能理論上這麼理解,特別是CPU數目少於線程數的機器而言,真正意義的併發是不存在的,各個線程只是斷斷續續地交替執行(以消耗CPU時間片的形式))。異步
隊列:ide
隊列是一種線性的數據結構,元素遵照「先進先出」原則。隊列的處理方式是處理完一個再處理下一個。測試
Swing程序中的線程
一個swing程序包含三種類型的線程:初始化線程(Initial Thread)、事件分派線程(Event Dispatch Thread)和任務線程(Worker Thread)。
初始化線程:每一個程序都有一個main方法,這是程序執行的入口,該方法運行在初始化線程上。初始化線程讀取程序參數並初始化一些對象。在許多Swing程序中,該線程主要目的是啓動程序的圖形用戶界面(GUI)。一旦GUI啓動後,對於大多數事件驅動的桌面程序來講,初始化線程的工做就結束了,程序的控制權就交給了UI。
事件分派線程(EDT ) :主要負責GUI組件的繪製和更新,並響應用戶的輸入。每一個EDT都會負責管理一個事件隊列(EventQueue),用戶每次對界面更新的請求(包括鍵盤、鼠標等事件)都會排到事件隊列中,而後等待EDT的處理。
任務線程:主要負責執行和界面無直接關係的耗時任務和輸入/輸出密集型操做,即任何干擾或延遲UI事件的處理都應該由任務線程來完成。注意,任務線程是經過javax.swing.SwingWorker類顯式啓動的。
EDT線程注意事項
1、任何GUI的請求都必須由EDT線程來處理
EDT線程將全部的GUI組件繪製和更新請求以及事件請求都放入了一個事件隊列中。隊列這種數據結構前面也講過了,它是線性、「先進先出」的。因此,經過事件隊列的機制,就能夠將併發的GUI請求轉化爲事件隊列,從而按順序處理。這樣就能夠保證線程安全。因此說,儘管大多數swing API自己不是線程安全的,可是swing經過EDT線程和事件隊列機制實現了保障線程安全。
同理,不建議從其餘線程直接訪問UI組件及其事件處理器,這會破壞線程安全的保障,可能會致使界面更新和繪製錯誤。
2、在非EDT線程中經過invokeLater和invokeAndWait方法向EDT線程的事件隊列添加GUI請求
有的時候須要在一個非EDT線程中調用swing API來處理GUI請求,根據第一條注意事項,顯然咱們不能直接訪問GUI組件。這時候咱們能夠調用這兩個方法將GUI請求添加到EDT線程的事件隊列中。
舉個例子:咱們有一個類A繼承了JFrame,如何在main方法中正確的啓動GUI呢?咱們知道main方法屬於初始化線程,這就是典型的非EDT線程訪問GUI組件的問題。
錯誤的啓動方式爲: new A();
若是這麼作了,就至關於在非EDT線程中直接訪問了GUI組件,這破壞了線程安全。
正確的啓動方式爲:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createGUI();
}
});
經過invokeLater和invoke方法,能夠從一個非EDT線程中,將GUI請求添加到EDT線程的事件隊列中去。
這兩個方法的區別是:invokeLater是異步的,調用該方法時,該方法將GUI請求添加到事件隊列中後直接返回。InvokeAndWait是同步的,調用該方法時,該方法將GUI請求添加到事件隊列中後,會一直阻塞,直到該請求被完成後纔會返回。
3、耗時操做應放到任務線程中,經過SwingWorker啓動任務線程
EDT的事件隊列的機制在保障了線程安全的同時,也引入了一個新的問題:假設事件隊列中某一個GUI請求執行時間很是長,那麼因爲隊列的特色,隊列中的後續GUI請求都會被阻塞。在實際的應用程序中,表現爲:點擊了一個按鈕觸發了耗時任務後其餘的組件都失去響應,必須等待該任務完成界面才能恢復響應。
咱們用一個簡單的程序測試一下,一個簡單的swing小程序。start按鈕模擬寫入數據(耗時操做),display按鈕用於將文本框中的內容輸出到文本顯示區中。數據寫入完成後,在文本框中輸出「數據寫入完畢」。在點擊start按鈕後,我連續點擊了三次display按鈕都沒有任何響應。三秒鐘以後,響應結果出來了,文本顯示區中輸出了三行「數據寫入完畢」。用戶體驗極差。
代碼以下:
package swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TestEDT { public static void createGUI() { JFrame frame=new JFrame("swing線程機制"); JTextField tf=new JTextField("hello world"); JTextArea ta=new JTextArea(); ta.setEditable(false);
ta.setPreferredSize(new Dimension(400,300)); JButton b1=new JButton("start"); JButton b2=new JButton("display"); JPanel p1=new JPanel(); JPanel p2=new JPanel(); p1.setLayout(new BorderLayout()); p2.add(b1); p2.add(b2); p1.add(ta,BorderLayout.NORTH); p1.add(tf,BorderLayout.CENTER); p1.add(p2,BorderLayout.SOUTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(p1);
frame.pack(); frame.setVisible(true); //start按鈕開始寫入數據,該操做耗時好久 b1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { //用線程休眠方法模擬耗時的寫入數據操做 try { Thread.sleep(3000); } catch (InterruptedException e1) { e1.printStackTrace(); } tf.setText("數據寫入完畢"); } }); //display按鈕用於將文本框中的信息輸出到文本顯示區中 b2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ta.append(tf.getText()+"\n"); } }); } //用正確的方式啓動GUI public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { createGUI(); } }); } }
考慮到用戶體驗性,應使用獨立的任務線程來執行耗時計算或輸入輸出密集型任務,好比同數據庫通訊、訪問網站資源、讀寫大數據量的文件等操做。
4、千萬別在EDT線程中調用invokeAndWait方法
在非EDT線程中,調用invokeAndWait方法能夠很好地將GUI請求添加到EDT線程的事件隊列中。可是若是在EDT線程中調用該方法會發生死鎖。
這是由於若是在EDT線程中調用invokeAndWait方法,GUI請求被添加到了事件隊列中後,invokeAndWait方法根據其特性,會一直等待直到EDT線程執行完本身run方法中的請求爲止。可是對於隊列而言,默認請求是被添加到尾部的。EDT線程根據到達不了該請求的位置,由於它如今的請求也就是invokeAndWait尚未執行完。
簡而言之,就是:EDT線程必須完成該方法後才能去完成該GUI請求,可是必須先完成該GUI請求才能完成該方法。這樣雙方互相等待,產生了死鎖。
任務線程的用法
任務線程是須要顯示地經過SwingWorker類調用的。
泛型參數<T,V>的分別表明:T 是 此 SwingWorker
的 doInBackground
和 get
方法返回的結果類型;V
是用於保存此 SwingWorker
的 publish
和 process
方法的中間結果的類型。
顧名思義,該類的doInBackground()方法表示在後臺執行的方法,由任務線程完成,用於執行耗時操做的代碼;done()方法是doInBackground方法執行完成後再調用的方法,方法體中的內容交付給EDT線程,用於處理GUI請求。
仍然以以前的模擬「寫入數據」程序爲例,演示任務線程的用法。
代碼以下:
package swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TestEDT { public static void createGUI() { JFrame frame=new JFrame("swing線程機制"); JTextField tf=new JTextField("hello world"); JTextArea ta=new JTextArea(); ta.setEditable(false);
ta.setPreferredSize(new Dimension(400,300)); JButton b1=new JButton("start"); JButton b2=new JButton("display"); JPanel p1=new JPanel(); JPanel p2=new JPanel(); p1.setLayout(new BorderLayout()); p2.add(b1); p2.add(b2); p1.add(ta,BorderLayout.NORTH); p1.add(tf,BorderLayout.CENTER); p1.add(p2,BorderLayout.SOUTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(p1); frame.pack(); frame.setVisible(true); //start按鈕開始寫入數據,該操做耗時好久 b1.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { new SwingWorker<Integer,Void>(){ protected Integer doInBackground() { //模擬寫入數據這一耗時操做 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } return 1; } protected void done() { tf.setText("數據寫入完畢"); } }.execute(); } }); //display按鈕用於將文本框中的信息輸出到文本顯示區中 b2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ta.append(tf.getText()+"\n"); } }); } //用正確的方式啓動GUI public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { createGUI(); } }); } }
測試結果爲:點擊start按鈕後,其餘的組件仍然有良好的響應。三秒鐘以後,文本框內容成功顯示爲「數據寫入完畢」。
總結一下,swing線程機制的注意事項有:
一、全部的GUI請求必須都由EDT線程完成(保障線程安全),不建議經過非EDT線程訪問GUI組件
二、非EDT線程經過invokeLater和invokeAndWait方法將GUI請求交付給EDT線程。
三、禁止在EDT線程中調用invokeAndWait方法(形成死鎖)。
四、耗時操做由任務線程執行,經過SwingWorker類顯示啓動任務線程。
over。