第七章: キャンセルとシャットダウン

MacFirefoxは本当に動作が不安定(しかも重いし・・・)。Safariに乗り換えたくなってきたけど、del.icio.usのエクステンションが無いのは辛いです。

キャンセルフィールドによる中断処理

こんな感じ?

public static void main(String[] args) {

	try {
		List<BigInteger> primes = new PrimeProducer().produceASecondOfPrimes();
		for (BigInteger p : primes) {
			System.out.println(p);
		}
	} catch (InterruptedException e) {
		Thread.currentThread().interrupt();
	}

}

private static class PrimeProducer {

	public List<BigInteger> produceASecondOfPrimes() throws InterruptedException {

		PrimeGenerator generator = new PrimeGenerator();
		new Thread(generator).start();
		try {
			Thread.sleep(1000);
		} finally {
			generator.cancel();
		}

		return new ArrayList<BigInteger>(generator.getPrimes());

	}

}

private static class PrimeGenerator implements Runnable {

	private volatile boolean canceled;
	private final List<BigInteger> primes = new ArrayList<BigInteger>();

	public void run() {
		BigInteger p = BigInteger.ONE;
		while (!canceled) {
			p = p.nextProbablePrime();
			synchronized (this) {
				primes.add(p);
			}
		}

	}

	public void cancel() {
		canceled = true;
	}

	public List<BigInteger> getPrimes() {
		return primes;
	}

}

インタラプション関連メソッド

Threadのstaticメソッド interrupted は、現在のスレッド(interruptedをコールしたスレッド)のインタラプションステータスをクリアし、その前の値を返す点に注意。
Producerがインタラプトされる(ブロックする)メソッドを使う場合、キャンセルのチェックにはインタラプションの仕組みを使うべき。↑のようなステータスのチェックでは、永遠にブロックされてしまう可能性があるため。

private static class PrimeProducer extends Thread {
	
	private final BlockingQueue<BigInteger> queue;
	
	public PrimeProducer(BlockingQueue<BigInteger> queue) {
		this.queue = queue;
	}
	
	@Override
	public void run() {
		try {
			BigInteger p = BigInteger.ONE;
			while (!Thread.currentThread().isInterrupted()) {
				queue.put(p.nextProbablePrime());
			}
		} catch (InterruptedException e) {
			// この PrimeProducer のインタラプションポリシーは、
			// 割り込まれたら終了すること。
			// → スレッドの終了を試みる
		}
	}
	
	public void cancel() {
		interrupt();
	}
	
}

インタラプションポリシー

タスクがインタラプションされるとき、それをキャンセルと見なすのかどうかというポリシーを持つべき。大事なことは、インタラプションステータスの保全である。通常、勝手にインタラプションされたという事実を揉み消し(例外を無視)してはいけない。
単純にInterruptedExceptionをクライアントに広めるのでなければ(そのままthrowするのでなければ)、catch節中でインタラプッテドステータスを復元すべき。

} catch (InterruptedException e) {
    // ステータスの復元
    Thread.currentThread().interrupt();
  ...
}

なお、このポリシーを知らないスレッドから、別のスレッドに対してinterruptメソッドを呼び出すのは良くない。ポリシーを知っている場合のみ割り込みすべき。

別スレッドでの例外の補足

あるスレッドで実行時例外が起こると、通常メインスレッドではその事実は気付かれない。
そこでタスクスレッドを生成するメソッドを切り出して例外を補足する方法もある。

public static void timeRun(final Runnable r, long timeout, TimeUnit unit) throws InterruptedException {

	class RethrowableTask implements Runnable {
		private volatile Throwable t;

		public void run() {
			try {
				r.run();
			} catch (Throwable t) {
				this.t = t;
			}
		}

		void rethrow() {
			if (t != null) {
				// throw Exception ...
			}
		}

	}
	
	// ... タスクの実行
	// タスクの終了を待機する
	taskThread.join(unit.toMillis(timeout));
	// 例外が発生していた場合は、再スローする
	task.rethrow();

}

タスクスレッドで発生した例外を補足してvolatileに保存しておき、タスクの終了後に例外が発生していたかチェックして、発生していた場合は再スローしている。
但し基本的にはこのような方法はとらずに、Futureを使用する方が簡単。

キャンセル処理のカプセル化

Javaでスレッドをブロックするメソッドは大抵割り込みに応答し、InterruptedExceptionをスローするようになっている。但し中には幾つか例外が存在するため注意が必要。
例えば同期I/O(ソケットのread/write)は応答しない。InputStream/OutputStreamはInterruptedExceptionをスローしない。但し、Socketをクローズすると、全てのブロックされているスレッドがSocketExceptioをスローするようになっている。このように、例外的な終了処理も存在する。
このような終了処理をカプセル化し、他の終了処理と同じように扱うためにThreadを拡張したり、或はThreadPoolExecutorのnewTaskForメソッドを使ってフックすると良い。

スレッドの異常終了

スレッドプールを扱うスレッドやSwingのイベントディスパッチスレッドは、実行時例外によって止まってしまうとアプリケーションが停止してしまう。対応としては

  • Throwableをcatchする
  • UncaughtExceptionHandlerを登録する

などがある。UncaughtExeptionHandlerが無ければ、通常アプリケーションはSystem.errにスタックトレースをプリントする。

JVMのシャットダウン

シャットダウンフックが動く場合は、並行に複数のフックスレッドが動くため、それぞれのフックはスレッドセーフでないといけない。
このため、通常シャットダウンフックは各サービス毎に登録するのではなく、全てのサービスに対応するフックを1つ登録しておく方が良い。

デーモンスレッド

通常アプリケーションが生成するスレッドは全て正規のスレッドである。
デーモンスレッドが正規のスレッドと異なるのは、JVMのシャットダウン時に意識されない点。あるスレッドが終了するとき、JVMは残りのスレッドを検査して、正規のスレッドが無ければ速やかにシャットダウンを開始し、残ったデーモンスレッドを破棄する。