きしだのはてな このページをアンテナに追加 RSSフィード

2008-12-07(日)

[][]SwingWorkerでスレッドからGUI操作 19:12 SwingWorkerでスレッドからGUI操作 - きしだのはてな を含むブックマーク

EventQueue#invokeLaterなどを使えということなのですが、実際の処理をいちいちinvokeLaterすると処理の記述が分断して大変みにくくなります。記述の美しさよりも処理の安全さ、とはいえ、これはあんまり。

ということで、Java SE 6からはSwingWorkerというクラスが用意されていて、便利に使えます。

使い方を図解すると、こう

f:id:nowokay:20081206172542p:image


関係ないけど、今回のサンプルはムダにNimbusです。

f:id:nowokay:20081206181946p:image


SwingWorkerは、Genericsパラメータをふたつ指定します。一つ目が処理結果の型で、二つ目が途中経過の型です。

new SwingWorker<Integer, int[]>()

SwingWorkerで実装するべきなのは、doInBackgroundメソッドです。ここでは、別スレッドで動かしたい処理を記述します。戻り値の型は、Genericsで一つ目に指定した型になります。

@Override
Integer doInBackground() throws Exception {

processメソッドは、途中経過を表示するメソッドです。引数にはGenericsで二つ目に指定した型のListが渡されます。

@Override
void process(List<int[]> chunks) {

publishメソッドで途中経過を渡すと、適当なタイミングでprocessメソッドが呼び出されます。

publish(new int[]{i, sum});

processメソッドが呼び出される頻度よりもpublishメソッドの方が多いとき、複数の出力がたまっている可能性もあります。そこで、出力はまとめて行うとなんとなく得した気分になります。

StringBuilder sb = new StringBuilder();
for(int[] values : chunks){
    sb.append(String.format(
            "%dを足して%d%n", values[0], values[1]));
}
taOutput.append(sb.toString());

出力は文字列で行うことが多いですが、最終的な出力文字列の生成はprocessメソッドで行ったほうがいいと思います。


処理が終わったら、doneメソッドが呼び出されます。

@Override
void done()

ここで、getメソッドを使うと処理結果が受け取れます。

int result = get();

このgetをdoneメソッド以外で呼び出した場合、doInBackgroundの処理が終わるまでブロックします。


処理の進捗はsetProgressメソッドで指定します。

setProgress(i * 10);

進捗が変わったときは、PropertyChangeEventが発生するので、PropertyChangeListenerを登録しておく必要があります。このときのイベントは"progress"です。

sw.addPropertyChangeListener(new PropertyChangeListener() {
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if("progress".equals(evt.getPropertyName())){
            progressBar.setValue((Integer)evt.getNewValue());
        }
    }
});

executeメソッドを呼び出すと、処理が開始します。

sw.execute();

ということで、ソース

import java.awt.BorderLayout;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
import javax.swing.SwingWorker;

public class SwingWorkerSample {
    public static void main(String[] args){
        //なんとなくNimbusを使う
        try {
            UIManager.setLookAndFeel(
                    "com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Exception ex) {
        }
        //ウィンドウ
        JFrame f = new JFrame("SwingWorkerサンプル");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //テキストエリア
        final JTextArea taOutput = new JTextArea(15, 30);
        JScrollPane sp = new JScrollPane(taOutput);
        f.add(sp);
        //プログレスバー
        final JProgressBar progressBar = new JProgressBar();
        f.add(BorderLayout.SOUTH, progressBar);
        //ボタン
        JButton b = new JButton("開始");
        f.add(BorderLayout.NORTH, b);
        
        //実行結果Integer, 処理経過データint[]のSwingWorker
        final SwingWorker<Integer, int[]> sw = new SwingWorker<Integer, int[]>(){
            /** バックグラウンド処理 */
            @Override
            protected Integer doInBackground() throws Exception {
                int sum = 0;
                for(int i = 1; i <= 10; ++i){
                    sum += i;
                    publish(new int[]{i, sum});//結果出力
                    setProgress(i * 10);//進捗
                    Thread.sleep(1000);
                }
                return sum;
            }

            /** 途中経過の表示 */
            @Override
            protected void process(List<int[]> chunks) {
                StringBuilder sb = new StringBuilder();
                for(int[] values : chunks){
                    sb.append(String.format(
                            "%dを足して%d%n", values[0], values[1]));
                }
                taOutput.append(sb.toString());
            }

            /** 処理終了 */
            @Override
            protected void done() {
                try {
                    int result = get();
                    taOutput.append("終了。" + result + "でした\n");
                } catch (InterruptedException ex) {
                } catch (ExecutionException ex) {
                }
            }

        };
        //プログレスバーの処理
        sw.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if("progress".equals(evt.getPropertyName())){
                    progressBar.setValue((Integer)evt.getNewValue());
                }
            }
        });

        //ボタンが押されたときの処理
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sw.execute();//SwingWorkderの実行
            }
        });

        //ウィンドウ表示
        f.pack();
        f.setVisible(true);
    }
}

2008-12-06(土)

[][]正しいスレッドプログラム 14:42 正しいスレッドプログラム - きしだのはてな を含むブックマーク

ここまでスレッドのサンプルを書いてきたのですが、主にプログラムがみにくくなるとか、めんどいとか、動くからえぇやんという理由で、やるべきことをやってないところがあります。

スレッドからのSwing操作とwaitの処理です。


Swingはシングルスレッドモデルで実装されているので、GUIスレッドとは別のスレッドからSwingを操作するべきではありません。EventQueue#invokeLaterなどを使って処理をGUIスレッドにのせる必要があります。(SwingUtilities#invokeLaterは内部でEventQueue#invokeLaterを呼び出しているだけです)

今回の一連のサンプルで使っているJTextField#setTextや、今回は使ってないけどよく使うJTextArea#appendなど、JTextComponentのテキスト操作に関しては、幸いスレッドセーフなのでそのまま使えますが、JFrame#setVisibleなどほとんどのSwingメソッドはスレッドセーフではありません。

mainメソッドからはじまるスレッドも、GUIスレッドとは違うので、今回のサンプルでは処理全体をinvokeLaterにしておくべきです。

たとえば処理全体をhogeメソッドに入れて

private static void hoge(){
  //処理
}

このメソッドをinvokeLaterから呼び出します

EventQueue.invokeLater(new Runnable(){
  @Override
  public void run(){
    hoge();
  }
}

もうひとつ、waitの問題。

waitがnotifyがなくても再開することがあるという問題で、JavaDocではObject#waitでは「スプリアスウェイクアップ」、Condition#awaitでは「見せかけの起動」とかかれてます。

そのため、Object#waitやCondition#awaitは、再開条件でのループで囲む必要があります。

これはEffective Javaで指摘されたことにより有名になりました。そのため、JDK1.4までのJavaDocには記述がありません。

http://sdc.sun.co.jp/java/docs/j2se/1.4/ja/docs/ja/api/java/lang/Object.html#wait()

http://java.sun.com/javase/ja/6/docs/ja/api/java/lang/Object.html#wait()


ということで、LockConditionSampleでこの問題に対応します。

とりあえずフラグを用意します。ここでは匿名クラスで使うためにfinalをつけるので、配列にしてます。

//ロック用オブジェクト
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
final boolean[] flag = {false};

awaitメソッドの呼び出しをwhileで囲みます。

flag[0] = false;
while(!flag[0]){
    condition.await();
}

で、signalメソッド呼び出し時にフラグを立てます。

try{
    lock.lock();
    flag[0] = true;
    condition.signal();
}finally{
    lock.unlock();
}

というか、これは各サンプルでちゃんとやっておこう。


全体のソース

import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.event.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.*;

public class LockConditionSample {
    public static void main(String[] args){
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                hoge();
            }
        });
    }
    private static void hoge(){
        JFrame f = new JFrame("Lockサンプル");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new FlowLayout());

        final JTextField tf = new JTextField();
        tf.setColumns(12);
        f.add(tf);

        //ロック用オブジェクト
        final Lock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();
        final boolean[] flag = {false};
        //スレッド
        final Thread t = new Thread(){
            @Override
            public void run() {
                try {
                    lock.lock();
                    for(int i = 3; i > 0; --i){
                        tf.setText("カウント" + i);
                        Thread.sleep(1000);
                    }
                    tf.setText("待機中");
                    flag[0] = false;
                    while(!flag[0]){
                        condition.await();
                    }
                    tf.setText("きた!");
                } catch (InterruptedException ex) {
                    tf.setText("中断");
                }finally{
                    lock.unlock();
                }
            }
        };
        t.start();

        JButton b;
        //再開ボタン
        b = new JButton("再開");
        f.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //ボタン押されたらnotify
                try{
                    lock.lock();
                    flag[0] = true;
                    condition.signal();
                }finally{
                    lock.unlock();
                }
            }
        });

        //中断
        b = new JButton("中断");
        f.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //ボタン押されたらinterrupt
                t.interrupt();
            }
        });

        f.pack();
        f.setVisible(true);
    }
}

2008-12-03(水)

[][]Lock/Conditionのサンプル 12:18 Lock/Conditionのサンプル - きしだのはてな を含むブックマーク

追記:「見せかけの起動」に対応しました。2008/12/6

http://d.hatena.ne.jp/nowokay/20081206#1228542137


wait/notifyでスレッド間の同期を行ったのですが、wait/notifyは対象オブジェクトでsynchronizedする必要がありました。

ということは、逆にいうとsynchronizedブロックひとつにつきひとつの同期オブジェクトしか使えないということです。


そこで、Lock/Conditionを使うともっと柔軟に同期オブジェクトを使うことができます。

とりあえず、wait/notifyサンプルと同じことをLock/Conditionでやってみました。

f:id:nowokay:20081203114802p:image


まず、Lockオブジェクトを用意します。

Lock lock = new ReentrantLock();

このLockオブジェクトのロック中に使える同期オブジェクトを、newConditonメソッドで得ます。

Condition condition = lock.newCondition();

また、「見せかけの起動」に対応するため、実行再開条件をあらわすフラグも用意しておきます。

boolean flag = false;

waitメソッドと同じ働きをするのがawaitメソッドです。awaitメソッドは再開条件の確認も行うようにします。

while(!flag){
    condition.await();
}

waitメソッドでsynchronizedが必要だったように、awaitメソッドではその元になったロックオブジェクトをlockしておく必要があります。

lock.lock();

notifyメソッドの代わりはsignalメソッドです。awaitの場合と同様にlockが必要です。ここで、waitの再開条件がみたせるようにしておきます。

try{
    lock.lock();
    flag = true;
    condition.signal();
}finally{
    lock.unlock();
}

ということでソース

import java.awt.FlowLayout;
import java.awt.event.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.*;

public class LockConditionSample {
    public static void main(String[] args){
        JFrame f = new JFrame("Lockサンプル");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new FlowLayout());

        final JTextField tf = new JTextField();
        tf.setColumns(12);
        f.add(tf);

        //ロック用オブジェクト
        final Lock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();
        final boolean[] flag = {false};
        //スレッド
        final Thread t = new Thread(){
            @Override
            public void run() {
                try {
                    lock.lock();
                    for(int i = 3; i > 0; --i){
                        tf.setText("カウント" + i);
                        Thread.sleep(1000);
                    }
                    tf.setText("待機中");
                    flag[0] = false;
                    while(!flag[0]){
                        condition.await();
                    }
                    tf.setText("きた!");
                } catch (InterruptedException ex) {
                    tf.setText("中断");
                }finally{
                    lock.unlock();
                }
            }

        };
        t.start();

        JButton b;
        //再開ボタン
        b = new JButton("再開");
        f.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //ボタン押されたらnotify
                try{
                    lock.lock();
                    flag[0] = true;
                    condition.signal();
                }finally{
                    lock.unlock();
                }
            }
        });

        //中断
        b = new JButton("中断");
        f.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //ボタン押されたらinterrupt
                t.interrupt();
            }
        });

        f.pack();
        f.setVisible(true);
    }
}

2008-12-02(火)

[][]wait/notify/interruptのサンプル 12:33 wait/notify/interruptのサンプル - きしだのはてな を含むブックマーク

追記:「スプリアスウェイクアップ」に対応しました。2008/12/6

http://d.hatena.ne.jp/nowokay/20081206#1228542137


ここまで、いろいろなロックを扱ったのですが、スレッド間での実行の制御も必要になります。

Javaでは、スレッド間の実行の制御にwait/notifyなどが用意されています。

ということで、wait/notifyなどのサンプルを作ってみました


プログラムを開始するとカウントダウンします。

f:id:nowokay:20081130205511p:image


カウントが0になると待機します。

f:id:nowokay:20081130205512p:image

このプログラムではロック用のオブジェクトを用意しています。また再開条件のフラグも用意します。

Object lock = new Object();
boolean flag = false;

待機するときは、オブジェクトのwaitメソッドを呼び出します。このオブジェクトに対してnotifyが呼び出されるまで、ここで待機します。このとき、notifyがなくてもwaitが再開してしまう「スプリアスウェイクアップ」という問題があるので、再開条件のチェックも必要です。

while(!flag){
    lock.wait();
}

waitメソッドは、そのオブジェクトでのsynchronizedブロックで使う必要があります。

synchronized(lock){

「再開」ボタンを押すと「きた」と表示されます。

f:id:nowokay:20081130205513p:image

このとき、lockオブジェクトのnotifyメソッドを呼び出しています。notifyメソッドも、呼び出すときには対象オブジェクトでsynchronizedしておく必要があります。再開条件を満たすようにフラグも設定します。

synchronized (lock){
    flag = true;
    lock.notify();
}

カウントダウン時や待機中に中断ボタンを押すと「中断」となります。

f:id:nowokay:20081130205514p:image

ここでは、スレッドのinterruputメソッドを呼び出します。

t.interrupt();

そうすると、sleepメソッドかwaitメソッドの実行時にInterruputedExceptionが発生します。

catch (InterruptedException ex) {
     tf.setText("中断");
}

このように、interrupt/InterruptedExceptionを使うと、スレッドをきれいに中断することができます。

スレッドを中断する命令としてstopメソッドがありますが、こちらは強制的にスレッドを中断します。finallyブロックは呼び出されますが、処理が中途半端になってしまう可能性があるので、非推奨になっています。


ところで、カウントダウン中に「再開」ボタンを押すとボタン処理が固まって、しばらくして「きた!」になります。

f:id:nowokay:20081130205510p:image

そういえば、waitメソッドもnotifyメソッドも、両方lockオブジェクトでsynchronizedしています。単純に考えると、このままではnotifyメソッドはロックが取得できず、デッドロックになってしまいそうです。

waitメソッドを呼び出すと、そのオブジェクトのロックが一時的に開放されます。そこでnotifyスレッドがロックを取得します。notifyを呼び出すと、そのnotifyスレッドがロックを開放してから、waitスレッドが再びロックを取得して実行が再開します。


ということで、ソース

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class NotifySample {
    public static void main(String[] args){
        JFrame f = new JFrame("Notifyサンプル");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new FlowLayout());

        final JTextField tf = new JTextField();
        tf.setColumns(12);
        f.add(tf);

        //ロック用オブジェクト
        final Object lock = new Object();
        final boolean[] flag = {false};

        //スレッド
        final Thread t = new Thread(){
            @Override
            public void run() {
                synchronized (lock){
                    try {
                        for(int i = 3; i > 0; --i){
                            tf.setText("カウント" + i);
                            Thread.sleep(1000);
                        }
                        tf.setText("待機中");
                        flag[0] = false;
                        while(!flag[0]){
                            lock.wait();
                        }
                        tf.setText("きた!");
                    } catch (InterruptedException ex) {
                        tf.setText("中断");
                    }
                }
            }

        };
        t.start();

        JButton b;
        //再開ボタン
        b = new JButton("再開");
        f.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //ボタン押されたらnotify
                synchronized (lock){
                    flag[0] = true;
                    lock.notify();
                }
            }
        });

        //中断
        b = new JButton("中断");
        f.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                //ボタン押されたらinterrupt
                t.interrupt();
            }
        });

        f.pack();
        f.setVisible(true);
    }
}

2008-12-01(月)

[][][]殺伐としたプログラム 09:51 殺伐としたプログラム - きしだのはてな を含むブックマーク

@daichan4649に「殺伐としたプログラムしか見ていないのでいつも癒されます」と言われたのだけど、「殺伐としたプログラム」ってどんなのだろう?

こうですか?わかりません ><

f:id:nowokay:20081130162753p:image


lockしたらunlockしません。早い者勝ち

ソースはこれ

import java.awt.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.*;

public class LockSample {
    private static Lock lock = new ReentrantLock();

    public static void main(String args[]) {
        JFrame f = new JFrame("殺伐としたプログラム");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new GridLayout(0, 1));

        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        f.setBounds((screenSize.width-400)/2, (screenSize.height-300)/2, 400, 300);

        for(int i = 0; i < 10; ++i){
            JTextField tf = new JTextField();
            f.add(tf);
            Runner r = new Runner();
            r.tf = tf;
            new Thread(r).start();
        }
        f.setVisible(true);
        
    }

    private static class Runner implements Runnable{
        JTextField tf;
        int speed = (int) (Math.random() * 100 + 50);

        @Override
        public void run(){
            try {
                String tr = "亡者>";
                for(int i = 0; i < 50; ++i){
                    tf.setText(tr);
                    tr = " " + tr;
                    Thread.sleep(speed);
                }
                try{
                    lock.lock(); //ここから先は1つしか同時に進めない
                    for(int i = 0; i < 50; ++i){
                        tf.setText(tr);
                        tr = " " + tr;
                        Thread.sleep(50);
                    }
                }finally{
                    //ロック開放しない!
                    //lock.unlock();
                }
            } catch (InterruptedException ex) {
            }
        }
    }

}

tockritockri 2008/12/01 11:11 癒された(嘘)

しましましましま 2008/12/02 00:47 フォークを持ったら手放さない哲学者を思い出しました.