- JAVA
InterruptedExceptionの正しい扱い方|握りつぶさない書き方
InterruptedException は「このスレッドに、止まってほしいという依頼(割り込み)が来た」という知らせです。 扱い方は2つしかありません。throws で呼び出し元に返すか、返せない場所なら catch の中で Thread.currentThread().interrupt() を呼んで割り込みフラグを立て直すかです。何もしない catch は、スレッドが止まらなくなる原因になります。
// 返せない場所(Runnable の中など)の基本形
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // フラグを立て直す
return; // その処理をやめる
}
| 書き方 | 判定 | 理由 |
|---|---|---|
throws InterruptedException を付けて返す |
◎ | 呼び出し元が止まるかどうかを決められる |
catch で interrupt() してから抜ける |
◎ | 外側のループや ExecutorService が割り込みに気づける |
catch で interrupt() してから別の例外で包む |
○ | 例外とフラグの両方で伝わる |
catch で別の例外に包むだけ |
△ | フラグが消えたまま。外側が気づけないことがある |
catch で何もしない・ログだけ |
× | shutdownNow() でもスレッドが止まらない(下で再現) |
確認環境:OpenJDK 1.8.0_504・21.0.10・25.0.4.1(Linux)で、この記事のプログラムを実際に動かして出力を載せています。仮想スレッドと ExecutorService.close() は JDK 21 で動かしました。API の説明は Java SE 21 の API ドキュメントで確かめています(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「InterruptedExceptionの正しい扱い方|握りつぶさない書き方」
割り込みの仕組み:フラグと例外
Java には、スレッドを外から安全に止める命令がありません。Thread.stop() は Java 21 の API ドキュメントで「常に UnsupportedOperationException を投げる」とされています。かわりに使うのが割り込み(interrupt)です。割り込みは「止まって」とお願いするだけで、止まるかどうかは、割り込まれた側のコードが決めます。
しくみは次の2つでできています。
- 割り込みフラグ:スレッドごとに1つある、真偽値の印。
t.interrupt()で立つ - InterruptedException:
sleepなどで待っている最中に割り込まれると投げられる。投げられた時点でフラグは消える
Java 21 の API ドキュメントでは、interrupt() は次のように動くと説明されています。
| 割り込まれたスレッドの状態 | 起きること |
|---|---|
wait・join・sleep で待っている |
フラグが消され、InterruptedException を受け取る |
InterruptibleChannel(FileChannel など)の I/O 中 |
チャネルが閉じられ、フラグが立ち、ClosedByInterruptException を受け取る |
Selector で待っている |
フラグが立ち、すぐに戻る(wakeup と同じ) |
| どれでもない(計算中など) | フラグが立つだけ |
最後の行が大事です。計算中のスレッドに割り込んでも、何も起きません。コードがフラグを見ない限り、止まりません。
フラグを見る3つのメソッド
| メソッド | 対象 | フラグを消すか |
|---|---|---|
t.interrupt() |
スレッド t |
立てる |
t.isInterrupted() |
スレッド t |
消さない |
Thread.interrupted()(static) |
今のスレッド | 消す |
JDK 21 で、フラグを立ててから2回ずつ呼んだ結果です。
Thread.currentThread().interrupt();
System.out.println("isInterrupted()=" + Thread.currentThread().isInterrupted() + " isInterrupted()=" + Thread.currentThread().isInterrupted());
System.out.println("interrupted()=" + Thread.interrupted() + " interrupted()=" + Thread.interrupted());
isInterrupted()=true isInterrupted()=true
interrupted()=true interrupted()=false
Thread.interrupted() は「見て、消す」メソッドです。 2回目は false になります。ループの条件にうっかり Thread.interrupted() を使うと、そこでフラグが消え、外側の処理が割り込みに気づけなくなります。条件に使うのは Thread.currentThread().isInterrupted() です。
なぜ catch でフラグを戻すのか
InterruptedException が投げられた時点で、フラグはもう消えています。catch で何もしなければ、「止まって」という依頼は、その場で消えてなくなります。
たとえば次のような処理で、内側のメソッド(ライブラリなど)が例外を握りつぶすと、外側のループは割り込みに気づけません。
// 外側:割り込まれたら止まるつもりのループ
while (!Thread.currentThread().isInterrupted()) {
pollAndProcess(); // 内側で sleep している
}
// 内側:握りつぶしている
void pollAndProcess() {
try { Thread.sleep(100); } catch (InterruptedException e) { /* 何もしない */ }
}
catch で Thread.currentThread().interrupt() を呼ぶと、消えたフラグが立ち直り、外側の isInterrupted() が true を返します。自分では止まらない場所でも、フラグだけは次の人に渡す、というのが「フラグを戻す」の意味です。
握りつぶすと止まらない:shutdownNow で再現
ExecutorService.shutdownNow() は、実行中のタスクに割り込みをかけて止めようとします。Java 21 の API ドキュメントでは「一般的な実装は Thread.interrupt() で取り消すので、割り込みに応じないタスクは終わらないかもしれない」と書かれています。実際に試しました。
import java.util.concurrent.*;
public class Sw {
public static void main(String[] a) throws Exception {
ExecutorService ex = Executors.newFixedThreadPool(1);
ex.submit(() -> {
int n = 0;
while (!Thread.currentThread().isInterrupted()) {
try { Thread.sleep(100); n++; }
catch (InterruptedException e) {
System.out.println("catch: " + e + " flag=" + Thread.currentThread().isInterrupted());
// ここで何もしない = 握りつぶし
}
if (n % 10 == 0) System.out.println("loop n=" + n);
}
System.out.println("exit n=" + n);
});
Thread.sleep(350);
System.out.println("shutdownNow");
ex.shutdownNow();
System.out.println("await=" + ex.awaitTermination(2, TimeUnit.SECONDS));
}
}
JDK 21 の出力です。
shutdownNow
catch: java.lang.InterruptedException: sleep interrupted flag=false
loop n=10
loop n=20
await=false
catch の時点で flag=false です。 フラグが消えているので、while の条件は true のままで、ループは回り続けます。2秒待っても終わらず(await=false)、main が終わっても、プール(非デーモンのスレッド)が残るので JVM が終了しませんでした(10秒のタイムアウトで強制終了するまで残りました)。
catch の中を Thread.currentThread().interrupt(); に変えた版と、同じ条件で比べた結果です。
| catch の中 | awaitTermination(2秒) | かかった時間 |
|---|---|---|
| 何もしない(握りつぶし) | false(終わらない) |
2000ms 待って打ち切り |
Thread.currentThread().interrupt() |
true |
11〜20ms |
握りつぶし版がいつも止まらないとは限らない点も、たちの悪いところです。 割り込みが sleep の外(n++ やループの条件を見ている間)に届くと、フラグは消えずに残り、ループを抜けます。4回動かしたうち1回はこの形で止まりました。「たまに終わらない」不具合は、この形を疑います。
ExecutorService との関係
| 操作 | タスクへの割り込み | 補足 |
|---|---|---|
shutdown() |
しない | 新しいタスクを受け付けず、今あるタスクは最後まで動かす |
shutdownNow() |
する(実行中のスレッドに interrupt()) |
待ち行列のタスクは返り値の List で返る |
awaitTermination(時間) |
しない | 終わるまで待つ。時間切れなら false |
close()(Java 19 以降) |
待っている間に自分が割り込まれたら shutdownNow() |
戻る前に自分のフラグを立て直す |
Future.cancel(true) |
する | cancel(false) は割り込まない |
CompletableFuture.cancel(true) |
しない | 実行中の処理はそのまま動く |
2段階で止める(API ドキュメントの例)
ExecutorService の API ドキュメントには、次の止め方が載っています。catch の中で shutdownNow() とフラグの立て直しをしているところが、この記事のテーマそのものです。
void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // 新しいタスクを受け付けない
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 実行中のタスクに割り込む
if (!pool.awaitTermination(60, TimeUnit.SECONDS))
System.err.println("Pool did not terminate");
}
} catch (InterruptedException ex) {
pool.shutdownNow();
Thread.currentThread().interrupt(); // フラグを残す
}
}
Future.cancel(true) と CompletableFuture.cancel(true) の違い
同じ cancel(true) でも、結果が違います。JDK 21 で、どちらも Thread.sleep 中のタスクを取り消しました。
Future.cancel(true): タスクに java.lang.InterruptedException: sleep interrupted
cancel(true)=true
cf.cancel(true)=true isCancelled=true
CompletableFuture: 最後まで眠った(cancelでは割り込まれない)
cf.join -> java.util.concurrent.CancellationException
CompletableFuture の API ドキュメントでは、cancel の mayInterruptIfRunning は「割り込みを処理の制御に使わないので、この実装では効果がない」と説明されています。CompletableFuture は「取り消し済み」になるだけで、動いている処理は止まりません。 止めたい処理は、自分でフラグや期限を見る形にします。
Future.get の InterruptedException は「呼び出し側」が割り込まれた知らせ
Future.get() が投げる InterruptedException は、待っている自分が割り込まれたという意味です。タスクが割り込まれたわけではありません。
get中に呼び出し側が割り込まれた: java.lang.InterruptedException / タスクは isDone=false
その後 get=result
タスクは動き続け、あとから結果を取れました。タスク側で起きた例外は、get() からは ExecutionException に包まれて出てきます。
close() は自分が割り込まれると shutdownNow する
Java 19 で入った ExecutorService.close()(try-with-resources で呼ばれる)を、1秒後に main を割り込む形で動かしました(JDK 21)。
try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
ex.submit(() -> {
try { Thread.sleep(3000); System.out.println("task: 最後まで眠った"); }
catch (InterruptedException e) { System.out.println("task: " + e); }
});
} // ここで close()
main を interrupt
task: java.lang.InterruptedException
close 戻り 1008ms flag=true
close() は、待っている間に割り込まれると、実行中のタスクに割り込みを伝え、自分のフラグを立てたまま戻ります。 API ドキュメントの「戻る前に割り込み状態を立て直す」のとおりです。逆に、タスクが割り込みを握りつぶすと、close() はいつまでも戻りません。
ExecutorService の使い方全体は、関連記事「Java ExecutorServiceの使い方|スレッドプールと仮想スレッド」で扱っています。
待つメソッドの比較表
先にフラグを立てておき、そのあと各メソッドを呼んだ結果です。sleep から LockSupport.park までの行は JDK 8・21・25 で同じ結果でした。synchronized・CompletableFuture.join・FileChannel・Socket の行は、JDK 21 で待っている最中に割り込んで確かめました。
| メソッド | 割り込まれたら | 戻ったあとのフラグ |
|---|---|---|
Thread.sleep / TimeUnit.sleep |
InterruptedException: sleep interrupted |
消える |
Object.wait |
InterruptedException |
消える |
Thread.join |
InterruptedException |
消える |
BlockingQueue.take / poll(時間) / put |
InterruptedException |
消える |
Future.get / CompletableFuture.get |
InterruptedException |
消える |
CountDownLatch.await / Semaphore.acquire |
InterruptedException |
消える |
Lock.lockInterruptibly |
InterruptedException |
消える |
Lock.lock |
例外なし。ロックを取って戻る | 残る |
synchronized の入口で待つ |
例外なし。BLOCKED のまま待ち続ける |
残る |
LockSupport.park / parkNanos |
例外なし。すぐ戻る | 残る |
CompletableFuture.join |
例外なし。待ち続ける | 残る |
FileChannel.read / write |
ClosedByInterruptException。チャネルが閉じる |
残る |
Socket の read(通常のスレッド) |
例外なし。読めるまで待ち続ける | 残る |
Socket の read(仮想スレッド) |
SocketException: Closed by interrupt |
残る |
読み取れることは3つです。
- フラグが立っていると、待つ前にすぐ例外になる。
sleep(1000)も 1 秒待たずに例外になりました。割り込みは「待っている最中」だけでなく「これから待つとき」にも効きます BlockingQueue.takeは、キューに要素があってもフラグが立っていれば例外になりました(JDK 8・21・25 のLinkedBlockingQueue)Lock.lock・synchronized・CompletableFuture.joinは割り込みで抜けません。取り消せる待ち方にしたいならlockInterruptibly()・tryLock(時間)・get(時間)を使います
synchronized の入口で待つスレッドに割り込んだ結果です。ロックが空くまで BLOCKED のままで、取れたあとにフラグが立っていることが分かります。
sync: interrupt後の状態 = BLOCKED
sync: ロックを取れた flag=true
FileChannel は、割り込まれるとチャネルそのものが閉じられます。
FileChannel.write: java.nio.channels.ClosedByInterruptException isOpen=false flag=true
ファイルを開いたまま使い回すつもりのコードでは、割り込みのあとにそのチャネルが使えなくなる点に気をつけます。
仮想スレッドでの振る舞い
仮想スレッド(Java 21 で正式に入った、軽いスレッド)でも、フラグと InterruptedException のしくみは同じです。
virtual sleep: java.lang.InterruptedException isVirtual=true
違いが出たのはソケットの読み込みです。相手が何も送らないソケットを read() している最中に割り込みました(JDK 21)。
platform: interrupt後1秒 isAlive=true
platform: read=-1
virtual: read -> java.net.SocketException: Closed by interrupt flag=true
virtual: interrupt後1秒 isAlive=false
| 項目 | 通常のスレッド | 仮想スレッド |
|---|---|---|
sleep・wait・BlockingQueue |
InterruptedException |
同じ |
ソケットの read |
割り込みでは抜けない(相手が閉じて -1) |
SocketException: Closed by interrupt、ソケットは閉じる |
newVirtualThreadPerTaskExecutor().shutdownNow() |
— | 実行中の全タスクに割り込みが届いた |
仮想スレッドでは、ネットワーク待ちのタスクも shutdownNow() や Future.cancel(true) で止まりやすくなります。そのぶん、割り込みを握りつぶすとソケットが閉じたまま処理が続くので、catch で何をするかが大事になります。仮想スレッドの使いどころは、関連記事「Java 仮想スレッド入門|10万タスク実測と使いどころ判断表」で扱っています。
throws で返すか、包むか:判断表
| 自分の立場 | おすすめ | 書き方 |
|---|---|---|
| 待つ処理を持つライブラリ・部品のメソッド | throws で返す | void take() throws InterruptedException |
Runnable.run() の中(throws できない) |
フラグを戻して抜ける | interrupt(); return; |
Callable.call() の中 |
throws で返す | call() は throws Exception なのでそのまま投げられる |
| インターフェースの都合で throws できない | フラグを戻し、別の例外で包む | interrupt(); throw new IllegalStateException(…, e); |
| 割り込まれても最後まで終わらせたい処理 | 待ち直し、最後にフラグを戻す | 下の「割り込まれても続ける」 |
| スレッドを自分で作って持っている(そのスレッドの持ち主) | ここでやめる | ログを出して run から戻る |
「割り込みで何をするか」を決めてよいのは、そのスレッドの持ち主だけです。プールのスレッドの持ち主は ExecutorService なので、タスクのコードは「フラグを残して早めに戻る」形にします。
包むときは、フラグも戻す
RuntimeException に包むだけの書き方と、フラグを戻してから包む書き方を、別スレッドで比べました(JDK 21)。
static void bad() {
try { Thread.sleep(1000); }
catch (InterruptedException e) { throw new RuntimeException(e); }
}
static void good() {
try { Thread.sleep(1000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("待機中に割り込まれた", e);
}
}
bad: java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted / flag=false
good: java.lang.IllegalStateException: 待機中に割り込まれた / flag=true
bad では、例外を受け取った側が catch (RuntimeException e) でログを出して続けると、フラグが消えているので、もう割り込みに気づけません。包むときも interrupt() を先に呼ぶと、どちらの経路でも伝わります。
割り込まれても続ける(途中でやめられない処理)
一度始めたら最後まで終わらせたい処理(取り消せない後片付けなど)は、割り込みを覚えておき、終わってからフラグを戻します。
public <T> T takeUninterruptibly(BlockingQueue<T> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true; // 覚えておいて、待ち直す
}
}
} finally {
if (interrupted) {
Thread.currentThread().interrupt(); // 最後に戻す
}
}
}
catch の中ですぐ interrupt() を呼んで待ち直すのは誤りです。 フラグが立った状態で take() を呼ぶと、前の表のとおりすぐ例外になるので、ループが空回りします。
よくある書き方の誤り
| 誤り | 何が起きるか | 直し方 |
|---|---|---|
catch (InterruptedException e) {} |
割り込みが消え、止まらない | interrupt() して抜ける |
catch で e.printStackTrace() だけ |
同上(ログが出るだけ) | 同上 |
catch (Exception e) でまとめて受ける |
InterruptedException も一緒に握りつぶされる |
InterruptedException を先に別の catch で受ける |
ループ条件に Thread.interrupted() |
条件を見た時点でフラグが消える | Thread.currentThread().isInterrupted() |
catch で interrupt() して、そのまま sleep を続ける |
すぐ例外になり、ループが空回りする | interrupt() したら抜ける |
while (true) で計算だけしている |
割り込みが来ても気づかない | 区切りごとに isInterrupted() を見る |
CompletableFuture.cancel(true) で止まると思う |
実行中の処理は止まらない | 期限・フラグを自分で見る |
よくある質問
Q. InterruptedException はなぜチェック例外なのですか。
A. 待つメソッドを呼ぶ人に、「割り込みが来たときにどうするか」を決めさせるためです。チェック例外なので、throws で返すか catch で受けるかをコンパイラが求めます。catch で受ける道を選んだら、フラグを戻すところまでがセットです。
Q. catch で Thread.currentThread().interrupt() を呼ぶと、また例外が出ませんか。
A. interrupt() はフラグを立てるだけで、例外は出ません。そのあとで sleep などの待つメソッドを呼ぶと、すぐに InterruptedException になります。そのため、フラグを戻したら、その処理からは抜けます。
Q. Spring の @Async や @Scheduled のメソッドでも同じですか。
A. 同じです。中身は ExecutorService などのスレッドプールで動くので、アプリの終了時にプールが止められるとき、割り込みを握りつぶしたタスクは終わりません。@Async の設定は関連記事「Spring Boot @Async|非同期の使い方と設定一覧・効かない原因表」で扱っています。
Q. 割り込みのかわりに volatile boolean のフラグを使うのはどうですか。
A. 自分のループを止める目印としては使えます。ただし sleep や take で待っている最中には届かないので、待ちを早く抜けるには割り込みも必要です。Java 21 の API ドキュメント(Thread.stop の説明)でも、変数で止める合図を出し、長く待つスレッドには interrupt を使う形が示されています。
Q. スレッドの基本から確かめたいです。
A. Thread の作り方・sleep・join は関連記事「Javaスレッドの作り方|Thread・Runnable・sleep・join」で、例外の種類の全体は関連記事「Java 例外一覧|種類・原因・対処の早見表と使い分け」で扱っています。
まとめ
- 割り込みは「止まって」という依頼。止まるかどうかは割り込まれた側のコードが決める
InterruptedExceptionが投げられた時点でフラグは消えている- 扱い方は
throwsで返すか、catchでThread.currentThread().interrupt()してから抜けるか - 握りつぶすと
shutdownNow()でも止まらず、JVM も終わらない。タイミングしだいで止まるので「たまに終わらない」不具合になる Lock.lock・synchronized・CompletableFuture.joinは割り込みで抜けない。CompletableFuture.cancel(true)は実行中の処理に割り込まない- 仮想スレッドでは、ソケットの
readも割り込みでSocketException: Closed by interruptになる - 包むときも、先に
interrupt()
参考・出典
確認日はいずれも 2026年10月13日です。
- Java SE 21 API「Thread」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html
- Java SE 21 API「InterruptedException」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/InterruptedException.html
- Java SE 21 API「ExecutorService」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ExecutorService.html
- Java SE 21 API「CompletableFuture」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/CompletableFuture.html
- Java SE 21 API「ClosedByInterruptException」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/channels/ClosedByInterruptException.html
- OpenJDK「JEP 444: Virtual Threads」:https://openjdk.org/jeps/444
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「InterruptedExceptionの正しい扱い方|握りつぶさない書き方」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。