【Java】joinメソッドでThreadが終了するまで待機する

Javaには並列処理を行うThread(スレッド)という便利な機能があります。Threadクラスのjoinメソッドを使用することで、別のスレッドの処理が終了するまで待機することができます。

ここでは、「別のスレッドの処理が終了するまで待機するjoinメソッドの使い方」について知りたい人のために、以下の内容で解説していきます!

  • スレッドとは
  • joinメソッドとは
  • joinメソッドの使い方
  • joinメソッドの注意点

今回はThread(スレッド)とjoinメソッドについてわかりやすく解説していますのでぜひ参考にしてください!

なお、Javaの記事については、こちらにまとめています。

目次

スレッドとは

スレッドとは、「複数の処理を同時並行で実施する機能」と考えるとよいかと思います。このスレッドが一列で順番に処理されていくことを、「シングルスレッド」と言います。

これに対して、スレッドの列が2つ以上複数あり並列に同時進行で処理されていくことを「マルチスレッド」と言います。使用する場合はThreadクラスを拡張したクラスを作り、runメソッドをオーバーライドして処理を定義します。

別のクラスでThreadクラスの拡張クラスをインスタンス化し、そのインスタンスメソッドであるstartメソッドを呼び出すことでrunメソッドの処理がマルチスレッドで開始されます。

簡単な記述例を示します。

// Threadクラスを拡張したThreadTestクラスを作成
public class ThreadTest extends Thread{
	// runメソッド
	public void run(){
		// オーバーライドで処理を定義		
	}
}
// マルチスレッドの実行クラス
class Exec{
	// Execクラスのメソッド
	void method(){
		// ThreadTestクラスのインスタンス化
		ThreadTest tt = new ThreadTest();
		// ThreadTestクラスのrunメソッドの処理開始
		tt.start();

	}
}

joinメソッドとは

joinメソッドとは、Threadクラスを拡張したクラスのインスタンスから使用できるメソッドのことです。Threadクラスを拡張したクラスではrunメソッドをオーバーライドして処理を定義します。

joinメソッドは、別のスレッドの処理が終了するまで待機させたい処理がある場合に使用します。

joinメソッドの使い方

それでは、joinメソッドを使って処理を待機させる例をみていきましょう。

// Threadクラスを拡張したThreadTestクラスを作成
public class ThreadTest extends Thread{
	public void run(){
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(1000); // 1秒待機
				System.out.println((i + 1) + "秒経過");
			} catch (InterruptedException e) {
				// 例外処理
				e.printStackTrace();
			}
		}
	}
}


public class Main {

	public static void main(String[] args) {
		// ThreadTestクラスのインスタンス化
		ThreadTest tt = new ThreadTest();

		// ThreadTestクラスの処理を実行
		tt.start();
		System.out.println("ThreadTestクラスの処理を開始しました");

		// ThreadTestクラスの処理が終了するまで待機の指示
		try {
			tt.join();
		} catch (InterruptedException e) {
			// 例外処理
			e.printStackTrace();
		}

		// Mainクラスの処理を実行
		System.out.println("ThreadTestクラスの処理が終了しました");
		System.out.println("Mainクラスの処理を開始します");
		for (int i = 0; i < 5; i++) {
			System.out.println((i + 1) + "回目の処理です");
		}
	}

}

実行結果:

ThreadTestクラスの処理を開始しました
1秒経過
2秒経過
3秒経過
4秒経過
5秒経過
ThreadTestクラスの処理が終了しました
Mainクラスの処理を開始します
1回目の処理です
2回目の処理です
3回目の処理です
4回目の処理です
5回目の処理です

この例では、Threadクラスの拡張クラスであるThreadTestクラスにおいて、1秒間隔で経過時間を表示する処理を定義しています。なお、Thread.sleepメソッドで待機させていますが、Thread.sleepメソッドはtry~catch構文で囲み、例外処理の形式で記述する必要があります。

try~catch構文がなければ、コンパイルエラーになりますので注意しましょう。Mainクラスでは、ThreadTestクラスをインスタンス化したttオブジェクトを生成しています。

ここで、ThreadTestクラスの処理を実行するためにstartメソッドを使用しています。startメソッドを使用することで、Threadクラスの拡張クラスの処理が実行されます。

次にjoinメソッドを使用することで、ThreadTestクラスの処理が終了するまで、Mainクラスの処理は待機となります。

実行結果をみても、「5秒経過」の後に「ThreadTestクラスの処理が終了しました」と表示され、その後にMainクラスの処理が表示されていることがおわかり頂けるかと思います。それでは、joinメソッドがない場合はどんな処理が行われるか、試しにみてみましょう。

// Threadクラスを拡張したThreadTestクラスを作成
public class ThreadTest extends Thread{
	public void run(){
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(1000); // 1秒待機
				System.out.println((i + 1) + "秒経過");
			} catch (InterruptedException e) {
				// 例外処理
				e.printStackTrace();
			}
		}
	}
}


public class Main {

	public static void main(String[] args) {
		// ThreadTestクラスのインスタンス化
		ThreadTest tt = new ThreadTest();

		// ThreadTestクラスの処理を実行
		tt.start();
		System.out.println("ThreadTestクラスの処理を開始しました");
		
		// Mainクラスの処理を実行
		System.out.println("ThreadTestクラスの処理が終了しました");
		System.out.println("Mainクラスの処理を開始します");
		for (int i = 0; i < 5; i++) {
			System.out.println((i + 1) + "回目の処理です");
		}
	}

}

実行結果:

ThreadTestクラスの処理を開始しました
ThreadTestクラスの処理が終了しました
Mainクラスの処理を開始します
1回目の処理です
2回目の処理です
3回目の処理です
4回目の処理です
5回目の処理です
1秒経過
2秒経過
3秒経過
4秒経過
5秒経過

この例では、前の例のMainクラスからjoinメソッドを取り除いています。

実行結果をみると、startメソッドが実行され「ThreadTestクラスの処理を開始しました」が表示された後、すぐにMainクラスの処理が始まり「Mainクラスの処理を開始します」が表示されています。

ThreadTestクラスのThread.sleepメソッドで1秒待機している間にMainクラスの処理は終了し、その後ThreadTestクラスの処理が行われています。スレッドで処理のタイミングを指定しなければ、このような意図したものとは異なる処理結果になってしまうのです。

joinメソッドの注意点

joinメソッドを使う場合は、いくつかの注意点があります。try~catch構文で囲み、例外処理の形式で記述する必要があります。

また、sleepメソッドで待機中にinterruptメソッドで割り込む場合にも例外処理の形式で記述する必要があります。interruptメソッドとは、スレッドを中断させるために使うメソッドです。中断しますが、止めるわけではありません。

待機中にinterruptメソッドで割り込まれると、InterruptedExceptionの例外が発生します。割り込み後の例外処理を「catch (InterruptedException e) { }」のように記述する必要があります。

joinメソッドはインスタンスメソッドと言って、インスタンス化されたオブジェクトからでないと使用できません。

前述の例のように、Threadクラスのsleepメソッドはインスタンス化せずに使用していますが、これに対してjoinメソッドはインスタンス化しなければ使用できません。これらの注意点を守らなければ、コンパイルエラーになりますので注意しましょう!

タイムアウトとデッドロックについて

joinメソッドは引数で待機時間(ミリ秒)を指定できます。これを指定すると、指定した時間以上になると処理の途中であっても次の処理へ移行します。これを「タイムアウト」と言います。

その他には、例えば2つのスレッドで互いのスレッドが終了するまで待機するようにすると、処理が終わらない場合が発生することがあります。これを「デッドロック」と言いますが、待機し続けて処理が終わらないので注意が必要です!

それでは、「タイムアウト」と「interruptメソッドでの割り込み」についても、例を挙げてみていきましょう。

// Threadクラスを拡張したThreadTestクラスを作成
public class ThreadTest extends Thread{
	public void run(){
		for (int i = 0; i < 5; i++) {
			try {
				Thread.sleep(1000); // 1秒待機
				System.out.println((i + 1) + "秒経過");
			} catch (InterruptedException e) {
				// 例外処理
				e.printStackTrace();
			}
		}
	}
}


public class Main {

	public static void main(String[] args) {
		// ThreadTestクラスのインスタンス化
		ThreadTest tt = new ThreadTest();

		// ThreadTestクラスの処理を実行
		tt.start();
		System.out.println("ThreadTestクラスの処理を開始しました");

		// 約3秒待機後にThreadTestクラスの処理が終了する前に次の処理へ移行
		try {
			tt.join(3000); // タイムアウト
			tt.interrupt(); // 中断
		} catch (InterruptedException e) {
			// 例外処理
			e.printStackTrace();
		}
		System.out.println("次の処理へ移行します");

		// Mainクラスの処理を実行
		System.out.println("Mainクラスの処理を開始します");
		for (int i = 0; i < 5; i++) {
			System.out.println((i + 1) + "回目の処理です");
		}

	}

}

実行結果:

ThreadTestクラスの処理を開始しました
1秒経過
2秒経過
次の処理へ移行します
Mainクラスの処理を開始します
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ThreadTest.run(Main.java:6)
1回目の処理です
2回目の処理です
3回目の処理です
4回目の処理です
5回目の処理です
4秒経過
5秒経過

この例の実行結果をみると、ThreadTestクラスの処理が終わる5秒間が経過する前に、joinメソッドにより約3秒(3000ミリ秒)後に別の処理に移行しています。また、interruptメソッドにより「3秒経過」を表示する処理が中断され、代わりに例外処理のメッセージが表示されています。

joinメソッドおよびinterruptメソッドは処理を中止するわけではないので、その後「4秒経過」、「5秒経過」と表示する処理が継続されているのがわかります。

Threadクラスの使い方総まとめ

この記事では紹介しきれなかったThreadクラスのいろいろな使い方を次の記事にまとめているので、ぜひ確認してください!

まとめ

ここでは、スレッドについて、joinメソッドの使い方や使う上での注意点について説明しました。

Java入門のサンプルコードでは、スレッドを指定しないコードの方が多くみられます。ただ実務を行う上では、前述の画面表示の例のように処理のタイミングを合わせるためにスレッドを指定する場合も多くみかけます。

使い始めの頃は慣れずに戸惑ったり、注意点に気づかない場合なども多くなるかもしれませんが、そんな場合はこの記事を何度も参考にして下さいね!

この記事を書いた人

熊本在住のフリープログラマ兼ライターです。C/C++/C#、Java、Python、HTML/CSS、PHPを使ってプログラミングをしています。専門は画像処理で最近は機械学習、ディープラーニングにはまっています。幅広くやってきた経験を活かしてポイントをわかりやすくお伝えしようと思います。
お問合せはこちらでも受け付けています。
info@sss-lab.com

目次