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. 割り込みフラグ:スレッドごとに1つある、真偽値の印。t.interrupt() で立つ
  2. 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?