Java好き

カテゴリ: 並行処理

スレッドを実行するならExecutorフレームワーク。

スポンサードリンク

概要

スレッドを効率的に実行可能なExecutorフレームワークが、
JavaSE1.5以降では利用できる。

古い入門書ではThread.start()が使われていたりするが、
JavaSE1.5以降であれば、Thread.start()ではなくExcecutorを使う。

Exectorフレームワークの中心は、スレッドプール

Thread.start()に比べて、スレッドプールには次のような利点がある。

スレッドの作成と破棄にはオーバーヘッドがかかる。
スレッドを無秩序に作成してしまうと、このオーバーヘッドが馬鹿にならなくなり、
逆に処理が遅くなってしまうこともある。

そこでスレッドプールを利用して、スレッドを再利用する。

スレッドを再利用することによって、
スレッド作成のオーバーヘッドや作りすぎによるメモリ不足に陥るのを防ぐことができる。
オーバーヘッドによる時間やメモリの節約という面では、DBのコネクションプールと同じような発想。

Exctorフレームワークではこのスレッドプールに加えて、
ライフサイクルを管理するメソッドなども提供するのでそれだけでも使う価値がある。

スレッドプール

Executorフレームワークでは、いくつかのスレッドプールの実装が用意されている。
Executorsクラスのファクトリメソッドを利用して、生成する。

おおまかな種類と特徴は次の通り。

ファクトリメソッド 特徴
newFixedThreadPool
newCachedThreadPool
newSingleThreadPool
newScheduledThreadPool

基本的な使い方

スレッドをファクトリメソッドから取得して、実行する。
このとき、shutdown()を忘れないことが重要。

public class SimpleExecutor {
    private static final Logger log = LoggerFactory.getLogger(SimpleExecutor.class);
 
    public void start() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
         
        try {
            executorService.execute(new SimpleRunnnable());
            executorService.execute(new SimpleRunnnable());
        } finally {
            executorService.shutdown();
            executorService.awaitTermination(1, TimeUnit.MINUTES);
        }
    }
 
    static class SimpleRunnnable implements Runnable {
        @Override public void run() {
            log.debug("スレッド終了");
        }
    }
 
    public static void main(String[] args) throws Exception {
        SimpleExecutor simpleExecutor = new SimpleExecutor();
        simpleExecutor.start();
    }
}

いきなりshutdown()することに慣れていないと奇妙に思えるが、これでいい。

ExecutorServiceは、タスクを依頼したスレッドとはまた別のスレッドで実行される。
(実装によっては同じスレッドもありえるが、別スレッドの方が普通。)

よって、shutdown()せずに実行だけしてしまうと、
Executor自体のスレッドを停止できない自体が発生してしまう。

JVMの仕様では実行中のスレッドが全て終了するまでJVMは終われないので、
依頼したタスクが終了してもExecutorのスレッドを停止できずにJVMを自然に終了できなくなる。

よって、shutdown()が重要。

実はシャットダウンに関するメソッドは2つある。
shutdown()とshutdownNow()。

平たく言うと、shutdown()は今依頼してあるタスクが終了したら終了してくださいというぐらい話。
一方、shutdownNow()は直ちに終了せよという意味。

だから、shutdown()を利用している上記の例では
タスクが終了したときに終わるようになっている。

shutdown()というメソッド名から想像すると、
即終了のイメージだが実際はそうではないので要注意。

最後のawaitTermination()は、万が一スレッドが長時間終了できなかった場合用のブロック処理。
これを入れておくことで、指定時間にタイムアウトで終わらせることが可能。

スレッドセーフを簡単にまとめました。

スレッドセーフとは

複数のスレッドからアクセスされたときに、
実行のタイミングがどうであっても、
呼び出し側に同期化の努力が必要なく正しく振る舞えるクラスのこと。

スレッドセーフを考えるときに一番問題なのは、
オブジェクトのステート(フィールド)へのアクセス。

複数のスレッドによる無秩序なアクセスがあっても、
正しい状態を維持し続けられるようにクラスを作ることが
すなわち、スレッドセーフなクラスを作成することになる。

このとき、synchronizedや明示的ロックやアトミック変数などを利用する。

スレッドセーフな例

サーブレット

ステートレスなサーブレットは共有するステートがそもそもないので、
スレッドセーフである。

サーブレットのポリシーに従えば、
サーブレット自体にフィールドを持たないようになっている。

このためリクエストのたびにスレッドが生成されるサーブレットでも
スレッドセーフとなる。

スレッドセーフではない例

競り合い状態

状態をチェックして次の行動を決める場合は、
スレッドセーフにならない場合が多いので要注意。

public class RaceCondition {   
    private static RaceCondition instatnce = null;
     
    public static RaceCondition getInstance(){
        if(instatnce == null)
            instatnce = new RaceCondition();
        return instatnce;
    }
}

この例は、マルチスレッドに対応していないシングルトンパターン。

一見すると正しく動く(インスタンスが1つに保たれる)気がするが、
スレッドセーフではない典型的な例。

次の順で実行されると、簡単にインスタンスが複数作れる。

1 2 3 4
スレッドA instatnce == null→true instatnce = new RaceCondition();
スレッドB instatnce == null→true instatnce = new RaceCondition();

このように状態をチェックして
次のアクションに移るまでのタイムラグがあると、
そのすきに他のスレッドがアクションが起こる前の状態をチェックして
不正な状態に持ち込まれてしまうことになる。

この例を修正するには、
メソッドにsynchronizedを指定すればよい。(他にも方法はある)

そうすると、必ず状態をチェックしてアクションした後でなければ
次のアクセスが実行されないので、
この例では正しさが保たれることになる。