【Java】スレッドの基本と生成方法

スポンサーリンク

スレッドの基本と生成についてまとめました。

スレッドとは

プログラムの実行状態をスレッドといい、すべてのプログラムはスレッドによって実行されています。 javaコマンドが実行されると JVM は新しいスレッドを作成し、そのスレッドによって指定したクラスのmainメソッドが実行されます。スレッドは main メソッドから始まるスタックトレースを持ち、最初から順番に命令を実行していき、main メソッドの実行が終了するとスレッドは消滅します。1つのスレッドはあくまでも1つの処理だけであり、2つの処理を同時に行うことはありません。

また、各スレッドは概念上、同時かつ平行で実行されます。CPU(コア)が復数あれば物理的にも並行動作しますが、CPU数以上の場合、スレッドはタイムスライスと呼ばれる動作をします。タイムスライスとは、短い時間間隔で実行するスレッドを切り替える動作であり、これにより仮想的に並行動作しているように見えます。

スレッドの生成

スレッドの生成で一番シンプルで簡単な方法は以下の通りです。

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();  // スレッド開始
    }
}

しかしこのコードは生成されたスレッドが何もせずに終了するため意味がありません。Thread#start()メソッドを実行すると、内部ではThread#run()メソッドを実行しますが、デフォルトの実装は、Threadインスタンスの生成時にコンストラクタに渡されたRunnableオブジェクトのrunメソッドを実行するようになっています。つまり今回のように単純にThreadを引数なしで生成した場合、runメソッドは実行する処理がないためすぐに終了するということです。

以上のことから正しいスレッドを生成する方法は2通りあり、java.lang.Threadクラスを継承してrun()メソッドをオーバーライドしたサブクラスを作成するか、java.lang.Runnableインターフェースを実装するかです。

Thread クラスを継承する場合は、作成したクラスをそのまま引数なしのコンストラクタで生成してください。

public class ThreadSample {
    static class MyThread extends Thread {
        @Override
        public void run() {
            IntStream.rangeClosed(0, 100).forEach(System.out::print);
        }
    }
    public static void main(String[] args) {
        Thread thread = new MyThread();
        thread.start();
    }
}
// 実行結果
$ java ThreadSample
012345......99100

Runnable は実装が必要なメソッドを1つだけ持つインターフェースなので、いわゆる関数型インターフェースと呼ばれます。関数型インターフェースといえばラムダ式ですが、基本的にスレッドの実装をラムダ式ですることはないと思いますので、ここでは忘れてください。Runnable を実装したクラスを Thread のコンストラクタに渡してオブジェクトを生成します。

public class RunnableSample {
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            IntStream.rangeClosed(0, 100).forEach(System.out::print);
        }
    }
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}
// 実行結果
$ java RunnableSample
012345......99100

するとThreadの継承ととRunnableの実装どちらがいいんだという話しにもなりますが、基本はRunnableを使用して、Threadrun以外のメソッドもオーバーライドしたい場合に使用しましょう。

スレッドプール

多数のスレッドを生成したい場合、通常はスレッド生成のコストを考えて、性能を上げるために生成済みのスレッドを使い回す手法がありますが、これをスレッドプールと呼びます。

スレッドプールは一般的に標準ライブラリの  Executors (Java Platform SE 8) のファクトリメソッドを使用して生成します。

ファクトリメソッド 内容
newFixedThreadPool 固定数のスレッドを再利用するスレッド・プールを作成。タスクは空いてるスレッドに割り当てられる。
newCachedThreadPool 必要に応じて新規スレッドを作成するスレッド・プールを作成。利用可能な場合には以前に構築されたスレッドを再利用して、一定期間使われないスレッドは消滅する。
newScheduledThreadPool タスクを一定時間ごとに実行するスレッドを持つスレッド・プールを作成。
newSingleThreadExecutor 単一のワーカー・スレッドを使用するexecutorを作成。タスクは順々に処理される。
newSingleThreadScheduledExecutor ScheduledThreadPool + SingleThreadExecutor
newWorkStealingPool work-stealingスレッド・プール(暇なスレッドが忙しいスレッドから自動的にタスクを奪うようなスレッド)を作成し、場合によっては競合を減らすために複数のキューを使用します。送信されたタスクの実行順序に関して何も保証しません。

Executors.newFixedThreadPoolで生成したExectorServiceを使用したサンプルは次の通りです。生成するスレッド数は 2 にして、Runnable の実装にはスレッド識別用の文字列を追加しました。2つのスレッドしかないので 3 つめのタスクは使用可能になるまでキューで待機していることを確認しましょう。

public class ThreadPoolSample {
    static class MyRunnable implements Runnable {
        private String name;
        MyRunnable(String name) { this.name = name; }
        @Override
        public void run() {
            IntStream.rangeClosed(0, 5).forEach(i -> {
                System.out.print(name + ":" + i + " ");
                try {
                    sleep(100);
                } catch (InterruptedException e) {}
            });
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new MyRunnable("one"));
        executor.execute(new MyRunnable("two"));
        executor.execute(new MyRunnable("three"));
    }
}
// 実行結果
$ java ThreadPoolSample
one:0 two:0 two:1 one:1 two:2 one:2 two:3 one:3 two:4 two:5 one:4 one:5 three:0 three:1 three:2 three:3 three:4 three:5 

ファクトリメソッドから返されたExectorServiceexecuteメソッドにRunnableオブジェクトを渡すと、内部で自動的にRunnableオブジェクトをスレッドに割り当てて実行します。スレッド作成はスレッドプール内に隠蔽されるので気にする必要はありません。

Callableインターフェース

Runnable オブジェクトの run メソッドは値を返しませんが、Callable インターフェースを使用するとスレッドから値を返すことができます。executeメソッドは戻り値を返しませんので、submitメソッドを使用します。戻り値はFutureオブジェクトのgetメソッドでCallable#runメソッドの戻り値が取得できます。

public class CallableSample {
    static class MyCallable implements Callable {
        @Override
        public String call() throws Exception {
            IntStream.rangeClosed(0, 100).forEach(System.out::print);
            return "finish";
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Future<String> result = executor.submit(new MyCallable());
        System.out.println(result.get());
    }
}
// 実行結果
$ java CallableSample
012345......99100finish

注意点として、Callable インターフェースは ExecutorService でしか使用することができないので、Runnable のように Thread のコンストラクタに Callable を実装したクラスを渡してスレッドを生成するということはできません。