今週は簡単なパズルから始めてみましょう。

サンプルのソースコード Monologue.java

ちょっと長いですが,全文を次に示します(インポート文は省略しています)。

public class Monologue {
    private JButton button;

    public Monologue() {
        JFrame frame = new JFrame("Monologue");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setLayout(new FlowLayout(FlowLayout.CENTER));

        button = new JButton("実行");
        button.setPreferredSize(new Dimension(100, 26));

        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                // 処理中であることを示すため
                // ボタンの文字列を変更し,使用不可にする
                button.setText("処理中...");
                button.setEnabled(false);
                     
                // ながーい処理
                try {
                    TimeUnit.SECONDS.sleep(10L);
                } catch (InterruptedException ex) {}
                 
                // 処理が終了したので,文字列を元に戻し
                // ボタンを使用可能にする
                button.setText("実行");
                button.setEnabled(true);
            }
        });

        frame.add(button);
        frame.setSize(100, 70);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        new Monologue();
    }
}

このプログラムを実行すると,フレームに「実行」と書かれたボタンが表示されます。

問題はこのボタンがクリックされた直後の状態はどうなるかということです。

クリックされたときには,イベント処理としてボタンの文字列を変更し,ボタンを使用不可にします。その後,何らかの処理を行うのですが,ここでは単純化のため単にスリープさせています。

スリープから目覚めると,ボタンの文字列を元に戻し,使用可能状態に戻します。

選択肢は図1の六つです。

(1)は「ボタンをクリックしても何も表示が変わらない」,(2)は「クリックされたままの状態になる」,(3)は「文字列が変化せずに使用不可状態になる」というものです。(4),(5),(6)は文字列が「処理中」になる以外は(1),(2),(3)と変わりません。

単純に考えれば,文字列を変更して,使用不可にしているので(6)になるはずですが,それだとパズルになりませんね。

さて,どれが正解でしょうか。

選択肢 1 選択肢 2 選択肢 3
(1) (2) (3)
選択肢 4 選択肢 5 選択肢 6
(4) (5) (6)
図1 パズルの選択肢

この問題は,サン・マイクロシステムズのJavaエバンジェリストグループが主体になって毎月開催しているセミナー「今月の2時間で学ぶJava Hot Topic」で取り上げたのと同じものです。Javaエバンジェリストグループは5分でわかる今週のJavaホットトピックというブログでも,定期的にJavaのパズルを掲載しており,このパズルも掲載されています。

ですので,このセミナーに参加された方はもう答えを知っていはずですね。正解は(2)です。

この結果は「Swingで実装されている」というところがキーです。同じものをAWTで作り直して実行すると,結果は(6)になります(ソースコード)。

なぜSwingだともともとの意図である(6)になってくれないのでしょうか。

それはSwingがシングルスレッドで実装されているからです。

SwingやAWTはイベント駆動で動作することは皆さんご存じのはずです。イベントはFIFO(First In, First Out)のキューにためられます。これをイベントキューと呼びます。

このイベントキューからイベントを取り出してイベント処理を実行するのがイベント・ディスパッチ・スレッドです。

ここまではSwingでもAWTでも同じです。ところが,AWTでは描画処理の多くをウィンドウ・システムに委譲しているのに対し,Swingではすべて自前で描画しているという違いがあります。

ウィンドウ・システムでの描画はイベント・ディスパッチ・スレッドとは別のスレッドで行われます。このため,AWTではボタンの文字列の変更はすぐに反映されます。

一方,Swingでは,イベント・ディスパッチ・スレッドが描画処理も行います。

上記のコードでマウスがクリックされたときのイベント処理を考えてみましょう。

マウス・ボタンがクリックされるとイベントキューにはActionEventが積まれます。そして,マウス・ボタンがリリースされ,MouseEventのMOUSE_RELEASEDと,MouseEventのMOUSE_CLICKEDが続いてキューに積まれます。

イベント・ディスパッチ・スレッドはActionEventを取り出し,actionPerformedメソッドに記述された処理を実行します。

ここでJButton#setTextメソッドがコールされると,setTextメソッドの内部でrepaintメソッドがコールされます。repaintメソッドは再描画イベントを発行し,イベントキューに積みます。

次にイベント・ディスパッチ・スレッドはJButton#setEnabledメソッドをコールします。これも同様に再描画イベントをイベントキューに積みます。

この時点では,まだMouseEventのMOUSE_RELEASEDは処理されていません。つまり,まだボタンがクリックされたままの状態になっているということです。

そして,「ながーい処理」がここで始まってしまいます。

つまり,マウスがリリースされた描画処理や,文字列の変更,使用不可のイベントはキューに積まれたまま,処理がブロックされてしまうのです。

したがって,選択肢(2)のままになってしまうというわけです。

この結果から,イベント処理を行うときには次の点に注意しなければならないことがわかります。

  • イベント処理では時間のかかる処理を実行しない

どうしても,長い時間がかかる処理を行う場合は,別にスレッドを用意して実行する必要があります。このときに注意しなくてはならないのは,Swingはスレッドセーフで作られていないということです。

つまり,別スレッドからJButton#setTextメソッドなどをコールすることはできないということです注1。他のスレッドからSwingのメソッドをコールするには,SwintUtilities.invokeLaterメソッドを介して行います。

ただ,ユーザーがイベント処理ごとにスレッドを作成し,SwingUtilities.invokeLaterメソッドで描画の更新を行うのは大変です。

前置きが長くなりました。ここでようやく登場するのがSwingWorkerクラスです。

SwingWorkerクラスは,Swingで簡単かつ安全にマルチスレッドを使用した非同期処理を行うことができるクラスです。

SwingWorkerクラスはもともとHans Muller氏とKathy Walrath氏による1998年の記事Threads and Swingの中でサンプルとして登場しました注2。その後,java.netでSwingWorkerプロジェクトになり,それがJava SE 6に取り入れられたという経緯があります。

さて,SwingWorkerクラスを使って,上記のパズル・プログラムを書き直してみましょう。

サンプルのソースコード Monologue2.java

public class Monologue2 {
    private JButton button;
 
    public Monologue2() {
        JFrame frame = new JFrame("Monologue");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new FlowLayout(FlowLayout.CENTER));
         
        button = new JButton("実行");
        button.setPreferredSize(new Dimension(100, 26));
 
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                // 処理中であることを示すため
                // ボタンの文字列を変更し,使用不可にする
                button.setText("処理中...");
                button.setEnabled(false);
         
                // SwingWorkerを生成し,実行する
                SwingWorker worker = new LongTaskWorker(button);
                worker.execute();
            }
        });
 
        frame.add(button);
        frame.setSize(100, 70);
        frame.setVisible(true);
    }
 
    // 非同期に行う処理を記述するためのクラス
    class LongTaskWorker extends SwingWorker<Object, Object> {
        private JButton button;
        
        public LongTaskWorker(JButton button) {
            this.button = button;
        }
         
        // 非同期に行われる処理
        @Override
        public Object doInBackground() {
            // ながーい処理
            try {
                TimeUnit.SECONDS.sleep(10L);
            } catch (InterruptedException ex) {}
             
            return null;
        }
         
        // 非同期処理後に実行
        @Override
        protected void done() {
            // 処理が終了したので,文字列を元に戻し
            // ボタンを使用可能にする
            button.setText("実行");
            button.setEnabled(true);
        }
    }
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new Monologue2();
            }
        });
    }
}

今週は少し長くなってしまったので,SwingWorkerクラスの解説は来週までのお楽しみということにしましょう。

今回はSwingWorkerクラスを使っている部分以外の変更点について,少しだけ言及しておきます。違いはmainメソッドのところです。

Monologueクラスのmainメソッドは,単にMonologueオブジェクトを生成していただけでした。しかし,よく考えてみると,Monologueクラスのコンストラクタで行われている処理はすべてSwingに関するものです。

しかし,Monologueクラスのコンストラクタはメインスレッドで実行されてしまいます。前述したように,Swingに関する処理はSwingのイベント・ディスパッチ・スレッドで実行しなくてはなりません。

そこで,Monologue2クラスでは,SwingUtilitiesクラスのinvokeLaterメソッドを使用しています。invokeLaterメソッドの引数はRunnableオブジェクトであり,このオブジェクトのrunメソッドはイベント・ディスパッチ・スレッドで実行されます。

これにより,Monologue2クラスのコンストラクタはイベント・ディスパッチ・スレッドで実行されます。

この部分は盲点になる場合が多いので,気をつけてください。

来週は,SwingWorkerクラスの使い方について解説します。


注1 Component#repaintメソッドなどイベント・ディスパッチ・スレッド以外のスレッドからコールされることを許されているメソッドもあります。

注2 Threads and Swingの続編として,Using a Swing Worker Threadと,Joseph Bowbeer氏によるThe Last Word in Swing Threadsがあります。どれもSwingでマルチスレッドを扱うのであれば,一読する価値がありますよ。

第28回を読む

著者紹介 櫻庭祐一

横河電機 ネットワーク開発センタ所属。Java in the Box 主筆

今月の櫻庭

この時期,気になるのは何といってもJavaOne。年に1回,Javaの最大のお祭りです。

今年は5月8日から11日。ゴールデンウィークの直後にサンフランシスコのモスコニセンターで開催されます。

今年はどんなサプライズが飛び出すのでしょうか。Java SE 7はどうなるのか,Java EE 6は。JRubyをはじめとするスクリプトも気になるところ。今から,ワクワクドキドキなのです。