Java Tips | 基本ユーティリティ:スリープユーティリティ

Java Java
スポンサーリンク

スリープユーティリティは「意図的に待つ」を安全にラップする道具

業務システムでも、「少し待ってから再試行したい」「ポーリング間隔を空けたい」「テストで時間経過をシミュレートしたい」といった場面で「スリープ(一定時間待つ)」が必要になります。
Java では Thread.sleep が基本ですが、そのまま使うと InterruptedException の扱いがバラバラになる・単位(ミリ秒/秒)が分かりにくい・テストしづらい といった問題が出やすいです。

そこで、「待つ」という行為をユーティリティに閉じ込めてしまうと、コードが読みやすくなり、例外方針も統一できます。
ここでは、初心者向けに「Thread.sleep をどうラップすると実務で気持ちよく使えるか」を、例題付きでかみ砕いて説明します。


Thread.sleep の基本と、そのまま使うとつらい理由

Thread.sleep の基本的な使い方

一番素朴なスリープはこうです。

try {
    Thread.sleep(1000);  // 1000ミリ秒 = 1秒
} catch (InterruptedException e) {
    // 割り込み時の処理
}
Java

Thread.sleep は「現在のスレッドを指定ミリ秒だけ止める」メソッドです。
ただし、InterruptedException を必ずキャッチしなければならず、呼び出すたびに同じような try-catch を書くことになります。

初心者がここでよく困るのが、「InterruptedException をどう扱えばいいのか分からない」「とりあえず catch して無視してしまう」という状態です。
これを放置すると、「割り込みが効かないスレッド」が生まれ、シャットダウンやキャンセルがうまく動かない原因になります。

InterruptedException を無視する危険性

よくあるアンチパターンは、こういう書き方です。

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // 何もしない
}
Java

これをやると、「スレッドに割り込みがかかっても、何事もなかったかのようにスリープを続ける」ことになります。
実務では、「アプリ終了時にスレッドを止めたい」「処理をキャンセルしたい」といった場面で割り込みが使われるので、これを無視するのはかなり危険です。

だからこそ、「InterruptedException をどう扱うか」をユーティリティ側で決めてしまい、呼び出し側にバラバラな判断をさせないことが大事になります。


実務で使えるシンプルなスリープユーティリティ

例外をラップして RuntimeException にするパターン

「とにかく簡単に『待つ』だけを使いたい」「割り込みは基本的に致命的とみなす」という方針なら、InterruptedException をラップして投げ直すユーティリティがよく使われます。

public final class Sleeps {

    private Sleeps() {}

    public static void millis(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 割り込みフラグを復元
            throw new IllegalStateException("Sleep was interrupted", e);
        }
    }

    public static void seconds(long seconds) {
        millis(seconds * 1000L);
    }
}
Java

使う側はこう書けます。

Sleeps.millis(500);  // 500ミリ秒待つ
Sleeps.seconds(3);   // 3秒待つ
Java

ここで深掘りしたいポイントは二つです。

一つ目は、InterruptedException をキャッチしたときに Thread.currentThread().interrupt() を呼んでいることです。
これは「割り込みフラグを復元する」ための定石で、上位のコードが割り込み状態を検知できるようにするために非常に重要です。

二つ目は、「ミリ秒」と「秒」の両方のメソッドを用意していることです。
Thread.sleep(1000) と書かれていると「これ何秒だっけ?」と一瞬考えますが、Sleeps.seconds(1) なら一目で分かります。
単位をメソッド名に乗せることで、読みやすさとバグ防止の両方に効いてきます。


TimeUnit を使った、より読みやすいスリープ

TimeUnit を使うと「何秒」「何分」が明示できる

java.util.concurrent.TimeUnit を使うと、「秒」「分」「ミリ秒」などの単位をコード上で明示できます。

import java.util.concurrent.TimeUnit;

public final class Sleeps {

    private Sleeps() {}

    public static void sleep(long time, TimeUnit unit) {
        try {
            unit.sleep(time);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Sleep was interrupted", e);
        }
    }

    public static void seconds(long seconds) {
        sleep(seconds, TimeUnit.SECONDS);
    }

    public static void millis(long millis) {
        sleep(millis, TimeUnit.MILLISECONDS);
    }
}
Java

使う側はこうなります。

Sleeps.sleep(5, TimeUnit.SECONDS);      // 5秒
Sleeps.sleep(200, TimeUnit.MILLISECONDS); // 200ミリ秒
Java

TimeUnit を使うメリットは、「単位変換を自分で計算しなくてよい」「コードを読んだときに意図が明確」という点です。
seconds * 1000L のような掛け算をあちこちに書くより、TimeUnit.SECONDS と書かれているほうが、後から読む人に優しいです。


リトライやポーリングと組み合わせるスリープユーティリティ

一定間隔でリトライする処理の中で使う

業務では、「外部 API が一時的に失敗したら、少し待ってから再試行する」といったリトライ処理がよく出てきます。
その中でスリープを使うときも、ユーティリティを挟んでおくとコードが読みやすくなります。

public class RetryExample {

    public void callWithRetry() {
        int maxAttempts = 3;
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                callExternalApi();
                return; // 成功したら終了
            } catch (TemporaryException e) {
                if (attempt == maxAttempts) {
                    throw e;
                }
                Sleeps.seconds(2); // 2秒待ってから再試行
            }
        }
    }

    private void callExternalApi() {
        // 外部API呼び出し
    }
}
Java

ここでのポイントは、「待つロジックを『Sleeps.seconds(2)』という一行に閉じ込めることで、リトライの意図が読みやすくなる」ことです。
Thread.sleep(2000) と書かれているより、「2秒待つ」という意図がはっきり伝わります。

ポーリング処理での利用

「キューにメッセージが来るまで 1 秒ごとにチェックする」といったポーリング処理でも、同じように使えます。

public void pollQueue() {
    while (true) {
        Message msg = queue.receive();
        if (msg != null) {
            handle(msg);
        } else {
            Sleeps.seconds(1); // メッセージがなければ1秒待つ
        }
    }
}
Java

こうした「待ちながら繰り返す」処理は、スリープの使い方を間違えると CPU を無駄に使ったり、割り込みに反応しなくなったりします。
ユーティリティを通すことで、「割り込み時の扱い」「単位」「ログ出力」などを一箇所で制御できるようになります。


テストしやすいスリープ設計という視点

実際には待たずに「待ったことにする」テスト

Thread.sleep をそのまま使うと、テストが遅くなります。
例えば「5秒待つ」処理をテストするたびに、本当に 5 秒待っていたら、テストスイート全体が耐えられません。

そこで、「スリープをインターフェース化して、テスト時には『何もしない実装』に差し替える」という設計もあります。

public interface Sleeper {
    void sleep(long time, TimeUnit unit);
}

public class RealSleeper implements Sleeper {
    @Override
    public void sleep(long time, TimeUnit unit) {
        try {
            unit.sleep(time);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(e);
        }
    }
}

public class NoopSleeper implements Sleeper {
    @Override
    public void sleep(long time, TimeUnit unit) {
        // 何もしない(テスト用)
    }
}
Java

業務コードでは Sleeper を受け取るようにしておき、本番では RealSleeper、テストでは NoopSleeper を渡す、という形です。
ここまでやるかどうかはプロジェクトの規模次第ですが、「スリープも依存性として差し替えられる」という発想を持っておくと、テスト設計の幅が広がります。


スリープユーティリティで気をつけるべきポイント

割り込みフラグを必ず復元する

InterruptedException をキャッチしたときに Thread.currentThread().interrupt() を呼ぶのは、実務ではほぼ必須の作法です。
これをしないと、「割り込みがかかったことを上位のコードが検知できない」状態になり、シャットダウンやキャンセルが効かなくなります。

ユーティリティの中でこの処理を必ず行うようにしておけば、呼び出し側は「割り込みがあったら IllegalStateException が飛ぶか、スレッドの割り込みフラグが立っている」と期待できます。

「スリープで調整するべきでないもの」もある

スリープは便利ですが、「本来は別の仕組みで制御すべきものを、スリープでごまかす」ことも起きがちです。
例えば、「ロックの代わりにスリープで待つ」「イベント駆動にすべきところをポーリング+スリープで済ませる」といった設計は、長期的には負債になりやすいです。

スリープユーティリティはあくまで「必要な場面で、正しく、統一的に待つ」ための道具であって、「なんでもスリープで解決する魔法」ではない、という感覚を持っておくと健全です。


まとめ:初心者がスリープユーティリティで身につけるべき感覚

スリープユーティリティは、「Thread.sleep を直接ばらまかない」「割り込みと単位の扱いを一箇所に集約する」ための小さなけれど重要な道具です。

押さえるべきポイントは次の通りです。

Thread.sleep をそのまま使うのではなく、ユーティリティメソッド(Sleeps.millis / Sleeps.seconds など)に閉じ込める。
InterruptedException をキャッチしたら、必ず Thread.currentThread().interrupt() で割り込みフラグを復元する。
TimeUnit を使って「秒」「ミリ秒」などの単位をコード上で明示する。
リトライやポーリングなど、「待ちながら繰り返す」処理では、スリープユーティリティを通して意図を分かりやすく書く。
テストでは、必要に応じて「実際には待たないスリーパー」に差し替える設計も検討する。

タイトルとURLをコピーしました