スレッドの基本と生成についてまとめました。
スレッドとは
プログラムの実行状態をスレッドといい、すべてのプログラムはスレッドによって実行されています。 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
を使用して、Thread
はrun
以外のメソッドもオーバーライドしたい場合に使用しましょう。
スレッドプール
多数のスレッドを生成したい場合、通常はスレッド生成のコストを考えて、性能を上げるために生成済みのスレッドを使い回す手法がありますが、これをスレッドプールと呼びます。
スレッドプールは一般的に標準ライブラリの 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
ファクトリメソッドから返されたExectorService
のexecute
メソッドに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 を実装したクラスを渡してスレッドを生成するということはできません。