こんにちは!エンジニアの中沢です。
Javaにはマルチスレッドで処理を効率的に行うためのThreadクラスがあります。Threadクラスを使ってマルチスレッドの記述をすることで、複数の処理を並列に行う効率的な動作をさせることができます。この記事では、
マルチスレッドの使い方を知りたい
sleepメソッドでスレッドを一定時間停止する方法を知りたい
という基本的な疑問から、
- joinメソッドで別のスレッドの終了まで待機する方法
- interruptメソッドで割り込む方法
- synchronized修飾子でスレッドを同期する方法
- ThreadLocalでスレッド毎に値を保持する方法
などの応用的な使い方に関しても解説していきます。今回はこれらの方法を覚えるために、Threadクラスのさまざまな使い方をわかりやすく解説します!
なお、Javaの記事については、こちらにまとめています。
スレッド(Thread)とは
スレッドとは、プログラムの中のひとまとまりの処理の単位のことです。このスレッドが1列で順番に処理されていくことを、シングルスレッドと言います。
これに対して、スレッドが2列以上あり、複数の処理が並列に同時進行で処理されていくことをマルチスレッドと言います。
マルチスレッドの使い方
ここではマルチスレッドのプログラムを作成する方法を解説します。マルチスレッドのプログラムを作成するには、Threadクラスを継承したクラスを作る方法と、Runnableインターフェースを実装する方法とがあります。
サブクラスを使う方法
マルチスレッドのプログラムを作成するには、Threadクラスを継承したサブクラスを作り、そのクラスでrunメソッドをオーバーライドしてマルチスレッドの処理を記述します。
以下のように記述します。
class サブクラス名 extends Thread { public void run() { 処理 } }
Runnableインターフェースを実装する方法
マルチスレッドのプログラムを作成するもう一つの方法は、Runnableインターフェースを実装する方法です。Runnableインターフェースにはrunメソッドのみが定義されています。
以下のように記述します。
class クラス名 implements Runnable { public void run() { 処理 } }
startメソッドで実行する方法
Threadクラスを継承したクラスを使う場合、そのインスタンス化する必要があります。また、Runnableインターフェースを実装したクラスを使う場合は、まず実装したクラスをインスタンス化したオブジェクトを作成し、そのオブジェクトでThreadクラスをインスタンス化する必要があります。
Threadクラスのオブジェクトからstartメソッドを呼び出すことでrunメソッドの処理がマルチスレッドで開始されます。startメソッドを使わずに、直接runメソッドを呼び出すとシングルスレッドで処理が実行されるので注意してください。
サブクラスを使って実行する場合
サブクラスを使ってマルチスレッドの処理をする方法を次のプログラムで確認してみましょう。
public class Main { public static void main(String[] args) { MultiThread mt = new MultiThread(); mt.start(); for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); System.out.println("スレッド1の" + (i + 1) + "度目の処理"); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MultiThread extends Thread { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); System.out.println("スレッド2の" + (i + 1) + "度目の処理"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
実行結果:
スレッド1の1度目の処理 スレッド2の1度目の処理 スレッド1の2度目の処理 スレッド2の2度目の処理 スレッド1の3度目の処理 スレッド2の3度目の処理
このプログラムではマルチスレッドの処理を行うために、Threadクラスを継承したMultiThredクラスを作成し、mainメソッドからstartメソッドでマルチスレッドの処理を実行しています。
また、マルチスレッドで処理が行われていることが分かりやすくなるように、sleepメソッドを使って処理を1秒停止しています。sleepメソッドについては次の章で詳しく解説します。実行結果から、2つのスレッドの処理が同時に行われていることが確認できます。
Runnableを実装したクラスで実行する場合
Runnableインターフェースを実装したクラスを使ってマルチスレッドの処理をする方法を次のプログラムで確認してみましょう。
public class Main { public static void main(String[] args) { MultiThread mt = new MultiThread(); Thread thread = new Thread(mt); thread.start(); for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); System.out.println("スレッド1の" + (i + 1) + "度目の処理"); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MultiThread implements Runnable { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); System.out.println("スレッド2の" + (i + 1) + "度目の処理"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
実行結果:
スレッド1の1度目の処理 スレッド2の1度目の処理 スレッド1の2度目の処理 スレッド2の2度目の処理 スレッド1の3度目の処理 スレッド2の3度目の処理
このプログラムではマルチスレッドの処理を行うために、まずRunnableインターフェースを実装したMultiThredクラスを作成しています。実行するために、MultiThreadクラスのインスタンスを生成し、mtオブジェクトに格納しています。
このmtオブジェクトを使ってThreadクラスをインスタンス化し、オブジェクトthreadに格納しています。オブジェクトthreadからstartメソッドを呼び出して、マルチスレッドの処理を実行しています。
sleepメソッドでスレッドを一定時間停止する
ここではThreadクラスのsleepメソッドでスレッドを一定時間停止する方法を解説します。sleepメソッドは引数に停止させたい時間をミリ秒単位で指定して使用します。
(1000ミリ秒 = 1秒)
ただし、sleepメソッドは誤差が出るため高い精度が必要な場合には注意してください。また、sleepメソッドで待機中にinterruptメソッドで割り込みを行うと、InterruptedExceptionの例外が発生するので注意してください。
次のプログラムでsleepメソッドの使い方と誤差を確認してみましょう。
public class Main { public static void main(String[] args) { try { for (int i = 0; i < 5; i++) { long start = System.currentTimeMillis(); Thread.sleep(1000); long stop = System.currentTimeMillis(); System.out.println((stop - start) + " [ms]"); } } catch (InterruptedException e) { e.printStackTrace(); } } }
実行結果:
1001 [ms] 1000 [ms] 1001 [ms] 1000 [ms] 1000 [ms]
このプログラムの実行結果から、sleepメソッドで誤差が出ていることが確認できました。sleepメソッドでスレッドを一定時間停止する方法についてはこちらで詳しく解説しているので、ぜひ確認してください!
joinメソッドで別スレッドの終了まで待機
ここではjoinメソッドで別のスレッドが終了するまで待機する方法を解説します。joinメソッドは別のスレッドの処理が終了するまで待機したい場合に使用します。次のプログラムで確認してみましょう。
public class Main { public static void main(String[] args) { MultiThread mt = new MultiThread(); mt.start(); try { System.out.println("別スレッドの処理を待機します。"); mt.join(); System.out.println("別スレッドの処理が終わりました。"); } catch (InterruptedException e) { e.printStackTrace(); } } } class MultiThread extends Thread { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); System.out.println("スレッド2の" + (i + 1) + "度目の処理"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
実行結果:
別スレッドの処理を待機します。 スレッド2の1度目の処理 スレッド2の2度目の処理 スレッド2の3度目の処理 別スレッドの処理が終わりました。
このプログラムでは、joinメソッドを使って別スレッドの処理が終わるまで待機しています。実行結果から、別スレッドの処理が終わってからメッセージを表示する処理をしていることが確認できます。
joinメソッドで別のスレッドの終了まで待機する方法についてはこちらで詳しく解説しているので、ぜひ確認してください!
interruptメソッドで割り込む
sleepメソッドやjoinメソッドの待ち状態の時に割り込みをかけるのがinterruptメソッドです。サンプルコードで確認しましょう。
public class Main { public static void main(String[] args) { MultiThread mt = new MultiThread(); mt.start(); try { for (int i = 0; i < 3; i++) { long start = System.currentTimeMillis(); Thread.sleep(1000); mt.interrupt(); long stop = System.currentTimeMillis(); System.out.println((stop - start) + " [ms]"); } } catch (InterruptedException e) { e.printStackTrace(); } } } class MultiThread extends Thread { public void run() { for (int i = 0; i < 3; i++) { try { Thread.sleep(1000); System.out.println("スレッド2の" + (i + 1) + "度目の処理"); } catch (InterruptedException e) { System.out.println("割り込まれました"); } } } }
実行結果:
割り込まれました 1000 [ms] スレッド2の2度目の処理 1000 [ms] 割り込まれました 1000 [ms]
このプログラムでは、mainメソッドがsleepメソッドで1000ミリ秒待ったあとに、別スレッドmtへ割り込みをかけています。
スレッドmtも同じ1000ミリ秒待ってから出力表示するようにしていますので、誤差によっては処理が実行できて出力表示している場合と割り込まれて例外処理を行っている場合がランダムに発生しています。
synchronized修飾子でスレッドの同期
synchronized修飾子が付いたメソッドをメンバに持つオブジェクトはそのメソッドを実行している間、同じオブジェクト内のsynchronized修飾子が付いたメソッドからアクセスされないようにロックされます。
また、メソッド同士を同時に実行させないように、あるメソッドは待機状態に、別のメソッドは実行可能状態にと区別する必要があります。waitメソッドを使って待機状態にして、notify、notifyAllメソッドを使って実行可能状態にします。
notifyAllメソッドはwaitにより待機状態にある全てのスレッドを実行可能状態にします。notifyメソッドは待機状態にあるスレッドのうち一つを実行可能状態にします。どのスレッドを実行可能状態にするか選択することはできません。
それでは、synchronized修飾子を使ったスレッドの同期についてサンプルコードで確認しましょう。
public class Main { public static void main(String[] args) { PrintClass pc = new PrintClass(); MultiThread1 mt1 = new MultiThread1(pc); MultiThread2 mt2 = new MultiThread2(pc); mt1.start(); mt2.start(); } } class PrintClass { private boolean bl = false; public synchronized void method1(String str, int i) { while(bl == true) { try { wait(); // blがtrueの間待機 } catch (InterruptedException e) { } } System.out.println(str + "の" + (i + 1) + "度目の処理"); bl = true; // blにtrueを代入したあと、待機状態のスレッドを実行可能状態に notifyAll(); } public synchronized void method2(String str, int i) { while(bl == false) { try { wait(); // blがfalseの間待機 } catch (InterruptedException e) { } } System.out.println(str + "の" + (i + 1) + "度目の処理"); bl = false; // blにfalseを代入したあと、待機状態のスレッドを実行可能状態に notifyAll(); } } class MultiThread1 extends Thread { private PrintClass pc; public MultiThread1(PrintClass pc) { this.pc = pc; } public void run() { for (int i = 0; i < 3; i++) { pc.method1("スレッド1", i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MultiThread2 extends Thread { private PrintClass pc; public MultiThread2(PrintClass pc) { this.pc = pc; } public void run() { for (int i = 0; i < 3; i++) { pc.method2("スレッド2", i); } } }
実行結果:
スレッド1の1度目の処理 スレッド2の1度目の処理 スレッド1の2度目の処理 スレッド2の2度目の処理 スレッド1の3度目の処理 スレッド2の3度目の処理
このプログラムでは、2つの出力表示用のメソッドをそれぞれ別のスレッドから呼び出しています。1つ目のスレッドはこのメソッドを呼び出してからsleepメソッドを使って1000ミリ秒待機しています。もう一方のスレッドはメソッドを呼び出したあとに待機時間は設定していません。
2つのスレッドを実行した結果を確認すると、一方のスレッドは待機時間を設定してませんが、別のスレッドの待機が終了するまで実行せずに同期して待機していることがわかります。
ThreadLocalでスレッド毎に値を保持する方法
マルチスレッドでスレッド毎に値を保持するには、ThreadLocalクラスを使用します。ThreadLocalクラスを使用しない場合には、クラス変数の値が複数のスレッドで共有されるため、期待通りの動作にならない可能性があります。
ThreadLocalクラスでスレッド毎に値を保持する方法についてはこちらで詳しく解説しているので、ぜひ確認してください!
まとめ
いかがでしたか?今回はスレッドの使い方について解説しました。マルチスレッドを使うと効率的な処理を行うプログラムができるのでぜひ覚えてくださいね。もし、スレッドを使う方法を忘れてしまったらこの記事を確認してください!