天天看點

Swing中的線程探究-2

如何改善Swing程式的響應性

        在上一篇文章中,曾經說過不能在EDT中執行耗時的任務,主要的原因是因為當EDT在執行這些耗時任務的時候,不能及時更新UI界面,這個時候整個界面是處于“卡住”狀态的,不能接受任何事件(如鍵盤輸入等)和描繪界面,容易給使用者一種程式“死掉”的感覺,非常不友好,比如下面的代碼是不受歡迎的。

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        writeHugeData();                    // 執行耗時的寫檔案任務
        jLabel.setText("Writting data..."); // 試圖立刻改變 jLabel 的内容
    }
});
           

        這個時候我們應該很容易地想到使用一個新線程來處理writeHugeData()任務,即把耗時的任務從EDT中剝離出來,這個時候代碼變成

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        // 啟動一個新線程來處理耗時工作
        new Thread(new Runnable() {
            public void run() {
                writeHugeData();
                jLabel.setText("Writting data...");
            }
        }).start();
    }
});
           

        上面的代碼應該是很多人經常寫的,但是 注意這裡它違背了前面提到的一個注意事項:更新界面的操作必須由EDT中去完成 。也即上面修改jLabel狀态的代碼不能由建立的線程完成,否則程式随時存在着死鎖的危險。然後,我們可以将上面的代碼改成

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        new Thread(new Runnable() {
            public void run() {
                writeHugeData();
                // 将更新界面的代碼放到事件隊列中
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        jLabel.setText("Writting data...");
                    }
                });
            }
        }).start();
    }
});
           

        這個時候已經能解決所有問題了,但是如果這樣做的話每次更新界面都需要建立一個Runnable的執行個體,對程式的性能有點浪費。其實最佳的方法應該是讓一個線程做好準備,運作和等待完成任務,這個線程我們稱其為“任務線程”。EDT将負責搜集事件,使用同步化将其傳遞給等待的任務線程,并通過等待/通知機制來告訴任務線程有新的要求。當任務線程完成了請求後,它将通過invokeLater()方法将更新界面的代碼交給EDT去處理,然後任務線程将傳回并繼續等待下一個通知。也就是在這樣的背景下,SwingWorker類應運而生。

關于SwingWorke類

        SwingWorker類是在JavaSE6中才出現的,它的目的是為了簡化程式員開發任務線程的工作,SwingWorker可以與各種UI元件在多線程的環境下互動,而不用程式員去過多關注。一般使用SwingWorker的做法是建立一個SwingWorker的子類,然後重寫其doInBackground()、done()和process()方法來實作我們需要完成的功能。上面的代碼可以繼續修改為

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        new SwingWorker<Long, Void>() {
            protected Long doInBackground() {
                // 執行耗時的寫檔案任務
                return writeHugeData();
            }
            protected void done() {
                try {
                    jLabel.setText("Writting data...");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.execute();
    }
});
           

測試程式

        下面通過一個程式來測試下讓EDT去執行耗時任務所導緻的後果,如下

/*
 * SynSwingDemo.java
 *
 * Created on 2009/11/27
 */

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.util.concurrent.ExecutionException;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;

/**
 * 這是一個展示編寫 Swing 程式時因為在 EDT 中執行了長時間的時間而導緻
 * 界面無法及時更新的例子,例子通過對比來增強感受
 * @author zhouych
 * @since JDK 1.6
 */
public class SynSwingDemo extends JFrame {

    private JButton button;
    private JLabel jLabel;
    private JCheckBox checkBox;

    public SynSwingDemo() {
        super("EDT阻塞");
        initComponents();
        setSize(500, 200);
        setLayout(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private void initComponents() {

        jLabel = new JLabel("顯示資訊");
        jLabel.setBounds(10, 10, 300, 25);
        this.add(jLabel);

        checkBox = new JCheckBox("是否讓 EDT 阻塞");
        checkBox.setBounds(10, 50, 200, 25);
        this.add(checkBox);

        button = new JButton("點選執行長時間事件");
        button.setBounds(10, 90, 200, 25);
        this.add(button);

        button.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                jLabel.setText("程式正在寫檔案...");

                if (checkBox.isSelected()) {
                    // 如果讓 EDT 阻塞
                    // 這是一個非常不好的程式設計習慣
                    long time = writeHugeData();
                    jLabel.setText("耗費時間為: " + time);
                } else {
                    // 如果 EDT 不阻塞,則使用 SwingWorker 來提高程式響應性
                    // 我們應該提倡這種做法
                    new SwingWorker<Long, Void>() {
                        @Override
                        protected Long doInBackground() {
                            return writeHugeData();
                        }
                        @Override
                        protected void done() {
                            try {
                                jLabel.setText("耗費時間為: " + get());
                            } catch (InterruptedException e1) {
                                e1.printStackTrace();
                            } catch (ExecutionException e2) {
                                e2.printStackTrace();
                            }
                        }
                    }.execute();
                }
            }
        });
    }

    /**
     * 一個寫巨大資料量的方法,需要長時間執行
     */
    public long writeHugeData() {
        try {
            long startTime = System.currentTimeMillis();

            FileOutputStream fos = new FileOutputStream("file.dat");
            DataOutputStream dos = new DataOutputStream(fos);
            // 寫入資料
            for (int i = 0; i < 2000000; i++) {
                dos.writeDouble(Math.random());
            }
            dos.flush();
            dos.close();
            fos.close();

            long endTime = System.currentTimeMillis();
            long time = endTime - startTime;

            return time;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0L;
    }

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
            e.printStackTrace();
        }
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                SynSwingDemo frame = new SynSwingDemo();
                frame.setVisible(true);
            }
        });
    }
}
           

        在運作上述程式的時候,請注意:

        1)請對比當點選按鈕後jLabel的内容是否立刻改變;

        2)請對比在寫入資料過程中checkBox是否能改變值;

        3)請對比在寫入資料過程中的時候改變視窗的大小;

        4)……

        通過上面的測試,大家應該都已經初步了解到如何改善自己的Swing程式的互動性了,SwingWorker的作用非常大,在SUN的官方文獻裡有篇很不錯的文章《 Improve Application Performance With SwingWorker in Java SE 6 》 ,并且William Chen已經将其翻譯了出來,有興趣的可以看下。