マルチスレッド処理において、「A」の処理が完了するまで「B」の処理を待ち合わせるといったケースがあります。これを「スレッド間の待ち合わせ」と言い、並列で動作するスレッドを制御する上で重要な知識となります。
この記事では、Javaのwait, notify, notifyAllメソッドを使用して、マルチスレッド処理における待ち合わせを制御をする方法を解説します。
目次
スレッド間の待ち合わせをする3つのメソッド
Javaでは、すべてのクラスのベースとなるObjectクラスに、スレッド間の待ち合わせ制御で使用する3つのメソッドが用意されています。
wait メソッド
waitメソッドは、別のスレッドからnotifyまたはnotifyAllメソッドが呼び出されるまで、スレッドを一時停止するメソッドです。
notify / notifyAll メソッド
notifyおよびnotifyAllは、waitで一時停止させておいたスレッドを再開させるためのメソッドです。
notifyは、ウェイトセットにあるスレッド1つを再開させるメソッドで、複数のスレッドがwaitで待機している状況では、再開するスレッドはJVMによってランダムに選択されます。
notifyAllは、waitで一時停止しているスレッドを全て再開するメソッドです。
「wait」メソッドの使い方
最初は、waitメソッドの使い方を見ていきましょう。waitメソッドは、現在のスレッドを一時停止し、該当のオブジェクトに対しnotifyおよびnotifyAllメソッドが呼ばれるまで待機します。
synchronizedをメソッドに指定し、waitメソッドの呼び出し前に該当のオブジェクトのロックを取得していることも忘れないようしましょう。
また、スレッドに対する割り込みが入った時にInterruptedException例外が発生するため、これをcatchなどで処理します。
synchronized void sampleWait() {
try {
wait();
} catch (InterruptedException e) {
//例外処理
}
}
上のサンプルコードは、thisのクラス・インスタンスに対するwaitメソッドの呼び出しですが、オブジェクトを指定してwaitメソッドを呼び出す場合は、synchronizedブロックで該当オブジェクトのロックを取得後、waitを呼び出します。
void sampleWait(obj) {
synchronized(obj) {
try {
obj.wait();
} catch (InterruptedException e) {
//例外処理
}
}
}
タイムアウトを指定する
waitメソッドの引数にタイムアウト時間を指定すると、指定した時間まではスレッドを一時停止し、タイムアウト時間を過ぎてもnotifyまたはnotifyAllが呼び出されない場合は、スレッドを再開させます。
synchronized void sampleWait() {
try {
//タイムアウトを1000ミリ秒に指定して、waitメソッドを呼び出し
wait(1000);
} catch (InterruptedException e) {
//例外処理
}
}
「notify」メソッドの使い方
次は、waitでウェイトセットに入っているスレッドを再開させるnotifyメソッドの使い方を見ていきましょう。
waitメソッドを読んだスレッドはブロックされるため、別のスレッドを生成してnotifyメソッド呼び出します。またsynchronizedを指定してnotifyで再開させるオブジェクトのロックを獲得するのも忘れないようにしましょう。
synchronized void sampleNotify() {
notify();
}
notifyではInterruptedException例外をキャッチする必要はありません。
「notifyAll」メソッドの使い方
notifyAllもnotifyと同様です。
synchronizedで再開させるオブジェクトのロックを獲得してからメソッドの呼び出しを行います。
synchronized void sampleNotify() {
notifyAll();
}
wait / notifyを組み合わせて待ち合わせを実装
各メソッドの使用方法を確認したところで、waitとnotifyを使ってスレッドの待ち合わせを実装するサンプルコードを見ていきましょう。
import java.util.*;
import java.util.concurrent.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
// 「wait」および「notify」でロックするオブジェクト
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
DateTimeFormatter dtformat = DateTimeFormatter.ofPattern("HH:mm:ss");
// スレッドプールの作成
ExecutorService pool = Executors.newFixedThreadPool(5);
// 別スレッドを生成し、wait()を呼びだし、notify()が呼ばれるまで待機
pool.submit(() -> {
synchronized(obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.printf("%s: スレッドが再開されました", dtformat.format(LocalDateTime.now()));
});
// 現在時刻を出力
System.out.printf("%s: 開始\n", dtformat.format(LocalDateTime.now()));
// ここで3秒間待機
Thread.sleep(3000);
// notifyの呼び出し
synchronized(obj) {
obj.notify();
}
// スレッドの終了を待機
try {
pool.shutdown();
pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上のコードを実行すると次のような結果となります。
実行結果
----------------------
00:10:12: 開始
00:10:15: スレッドが再開されました
まとめ
Javaのwait、notifyおよびnotifyAllメソッドで、スレッド間の待ち合わせを実装する方法を解説してきました。
マルチスレッド処理においては、同時並行で複数の処理が実行されるため、最初は理解や考え方が難しいですが、何本かのサンプルコードを実際に自分の手で組んでみれば自然と身につくはずですので、是非挑戦してみましょう。
wait、notifyおよびnotifyAllメソッドを実行する場合は、synchronizedを使って、該当のオブジェクトのロックを取得しておく必要があります。