WisdomSoft - for your serial experiences.

9.4 待機と通知

複数のスレッドで処理を実行している工程で、wait() メソッドと notify() メソッドを用いてプログラムからスレッドの停止や再開を制御する方法について解説します。

9.4.1 スレッド間の通信

Object クラスには、スレッドの集合を管理する機能を標準で備えています。これを利用すれば、スレッドの待機と再開をプログラム的に、任意のタイミングで行うことができるようになります。この機能を待機集合と呼びます。

待機集合は、オブジェクトが生成された時点では空の状態です。スレッドが待機状態になると、この待機集合にスレッドが追加され、スレッドが保有するロックをすべてアンロックします。そして、他のスレッドの要求などで待機状態が解除されると、再びロックされてスレッドが走り出すという仕組みになっています。

これらの機能を利用するには Object クラスの wait() メソッド、notify() メソッド、notifyAll() メソッドを使います。これらのメソッドについては5.8 Objectクラス.表1を参照してください。Object クラスがこのような機能を提供しているという事実は、Java のすべてのオブジェクトが、スレッド間通信を実装することができることを意味しています。ただし、これらのメソッドを呼び出すにはスレッドがロックを行っていることが条件となります。ロックを行っているとは、すなわち synchronized 修飾子を指定したメソッド内か、synchronized 文のブロック内でのみ呼び出せるということです。

wait() メソッドを呼び出すと、スレッドが停止し、そのスレッドが保有するすべてのロックを開放して待機集合に追加されます。wait() メソッドを呼び出したスレッドは、その時点でロックを開放して待機するため、他のスレッドがブロックのロックを取得することができるようになります。

待機しているスレッドを再開するには、wait() メソッドの引数で指定した時間が経過するか、他のスレッドがオブジェクトの notify() メソッドを呼び出したときです。notify() メソッドを呼び出すと、待機集合の任意のスレッドが選択され、再開します。wait() メソッドの引数に 0 を指定するか、引数のない wait() メソッドを呼び出した場合は、notify() メソッドで明示的に通知されるまで再開することはできません。notifyAll() を用いれば、待機集合中のすべてのスレッドが再開します。ただし、再開するスレッドは実行中のスレッドがアンロックするまで実行することはできません。

これらのスレッド間通信は、sleep() メソッドを使った場合と異なり、他のスレッドによってスレッドを再開させるという明示的な同期制御を実現できることにあります。

コード1
class WaitTest {
	public synchronized void stop() {
		System.out.println(Thread.currentThread().getName() + ":待機します");
		try { wait(); }
		catch(InterruptedException err) { System.out.println(err); }
		System.out.println(Thread.currentThread().getName() + ":再開しました");
	}
	public synchronized void restart() {
		System.out.println(Thread.currentThread().getName() + ":再開通知");
		notify();
	}
}

class Test {
	public static WaitTest obj = new WaitTest();
	public static void main(String args[]) throws InterruptedException {
		for(int i = 0 ; i < 5 ; i++) {
			Thread stopThread = new Thread() { public void run() { obj.stop(); } };
			stopThread.start();
		}
		
		for(int i = 0 ; i < 5 ; i++) {
			Thread.sleep(1000);
			obj.restart();
		}
	}
}
実行結果
java Test
Thread-0:待機します
Thread-4:待機します
Thread-3:待機します
Thread-2:待機します
Thread-1:待機します
main:再開通知
Thread-0:再開しました
main:再開通知
Thread-4:再開しました
main:再開通知
Thread-3:再開しました
main:再開通知
Thread-2:再開しました
main:再開通知
Thread-1:再開しました

コード1では、WaitTest クラスの stop() メソッドを別のスレッドから呼び出し、内部でメソッドを実行しているスレッドを停止させます。stop() メソッド内で wait() メソッドも呼び出しスレッドを待機させてロックを開放します。Thread-0 は stop() メソッドの途中で中断され、通知を受けるまで復帰することはありません。stop() メソッドは Thread-0 から Thread-4 まで 5 つのスレッドから呼び出されています。これらのスレッドは、オブジェクトの待機集合に追加され、通知を待っています。

続いて main() メソッドを実行する主スレッドから sleep() メソッドで 1000 ミリ秒停止させた後に restart() メソッドを呼び出す処理を for 文で繰り返しています。これによって 1 秒ごとに restart() メソッドが実行され、大気集合にあるスレッドが順に再開されます。

これらのスレッド間通信を行うには、スレッドがロックを保有していなければならないことに注意してください。そうでなければ IllegalMonitorStateException がスローされます。sync() メソッドの synchronized 修飾子を外してコンパイルし、実行してみると、実行時例外がスローされることを確認できます。

synchronized や待機集合を用いた同期制御で注意しなければならないのは、スレッドがお互いにお互いの開放を待ちあうデッドロックと呼ばれるバグです。スレッド A はスレッド B の終了を待ち、スレッド B はスレッド A の終了を待っているという状態です。互いにロックの開放を待つため、永遠にスレッドが再開されることはありません。