Thread の割り込みフラグは InterruptedException の throw によりクリアされる

例えば自身で作っているコード内で、呼び出し元の処理をブロックしたいようなケースはよくある。

String send(final String msg) throws IOException {
  Thread thread = new Thread() {
    private response;
    public void run() {
      os.print(msg); // 送信に不定時間が必要
      response = is.readLine(); // 受信にも不定時間が必要
      synchronized (lock) {
        lock.notifyAll();
      }
    }
    public void toString() {
      return response;
    }
  };
  thread.start();
  synchronized (lock) {
    try {
      lock.wait(); // 通信中は処理をブロック
    } catch (InterruptedException e) {
      // ★ 今回はここのお話
    }
  }
  return thread.toString(); // 送信したメッセージに対する応答を返す
}

上記のような疑似コードがあったとしよう。通信を同期で処理したいというよくあるケースだ。

まず、wait() メソッドでブロックしているのは、当たり前だけど呼び出し元のスレッド。そして wait() は InterruptedException を catch しなければならない。これも当たり前。

呼び出し元スレッドが、仮に以下のような実装だった場合:

void sendMessage() {
  while (!Thread.isInterrupted()) {
    String msg = queue.poll();
    sender.send(msg);
  }
}

割り込みがあるまで、キューのメッセージを送信し続ける、という動作を期待するが、実はこのこれは期待通りには動作しない。

Thread#interrupt() は対象のスレッドが Object#wait()、Object#wait(long)、Object#wait(long, int)、Thread#join()、Thread#join(long)、Thread#join(long, int)、Thread.sleep(long)、Thread.sleep(long, int) を呼び出してブロックしている場合は InterruptedException が発生し、そうでなければ Thread.interrupted()、Thread#isInterrupted() で検知可能な割り込みフラグをセットする。

問題は、InterruptedException が発生した場合は割り込みフラグがセットされないばかりか、既存の割り込みフラグもクリアされてしまうが、呼び出し元では割り込みフラグを期待している場合(つまり上記例)だ。


こういった場合、どうすればよいか。

以下サイトに答えが書かれている。

InterruptedException をキャッチしたメソッドが、この (確認済みの) 例外をスローするように宣言されていない場合は、次のような決まった書き方により「自らに再割り込みする」必要があります。



   Thread.currentThread().interrupt();



これにより、スレッドは、可能なかぎり早く InterruptedException を再発行できるようになります。

Java 推奨されないスレッドプリミティブ

ブロッキング・メソッドが割り込みを検出して割り込み例外をスローするときには、割り込みステータスをクリアします。割り込み例外をキャッチしても再スローできない場合は、割り込みが発生した証拠を残す必要があります。この証拠により、呼び出しスタックの上位のコードが割り込みの発生を認識し、その割り込みに対する応答が可能になります。このタスクは、リスト3に示すように、interrupt() を呼び出して現在のスレッドに「再び割り込む」ことによって実行されます。少なくとも、割り込み例外をキャッチして再スローしないときには、リターンする前に常に現在のスレッドに対して再割り込みを行うようにします。

Javaの理論と実践: 割り込み例外の処理


つまり、あらゆる呼び出し元を想定したより良いコードは以下のようになる。

String send(final String msg) throws IOException {
  Thread thread = new Thread() {
    private response;
    public void run() {
      os.print(msg); // 送信に不定時間が必要
      response = is.readLine(); // 受信にも不定時間が必要
      synchronized (lock) {
        lock.notifyAll();
      }
    }
    public void toString() {
      return response;
    }
  };
  thread.start();
  synchronized (lock) {
    try {
      lock.wait(); // 通信中は処理をブロック
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt(); // ★ クリアされたフラグを再セットする
    }
  }
  return thread.toString(); // 送信したメッセージに対する応答を返す
}

Thread.currentThread().interrupt() でググるか、Oracle の実装を grep すると、このイディオムの意図が良くわかるはずだ。


おまけ:簡単な検証コードを書いてみた。

public class Interrupted {
    public static void main(String[] args) {
        final Thread mainThread = Thread.currentThread();
        mainThread.interrupt();
        System.out.println("1:" + mainThread.isInterrupted());
        new Thread() {
            public void run() {
                try {
                    Thread.sleep(1000);
                    mainThread.interrupt();
                } catch (InterruptedException e) {
                }
            }
        }.start();
        try {
            System.out.println("2:" + mainThread.isInterrupted());
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            System.out.println("3:" + mainThread.isInterrupted());
        }
    }
}

このコードは 3 回出力を行っているが、結果は以下のようになる。

1:true
2:true
3:false

Thread.sleep(long) で発生した InterruptedException で割り込みフラグがクリアされているのが見て取れる。