スリープユーティリティは「意図的に待つ」を安全にラップする道具
業務システムでも、「少し待ってから再試行したい」「ポーリング間隔を空けたい」「テストで時間経過をシミュレートしたい」といった場面で「スリープ(一定時間待つ)」が必要になります。
Java では Thread.sleep が基本ですが、そのまま使うと InterruptedException の扱いがバラバラになる・単位(ミリ秒/秒)が分かりにくい・テストしづらい といった問題が出やすいです。
そこで、「待つ」という行為をユーティリティに閉じ込めてしまうと、コードが読みやすくなり、例外方針も統一できます。
ここでは、初心者向けに「Thread.sleep をどうラップすると実務で気持ちよく使えるか」を、例題付きでかみ砕いて説明します。
Thread.sleep の基本と、そのまま使うとつらい理由
Thread.sleep の基本的な使い方
一番素朴なスリープはこうです。
try {
Thread.sleep(1000); // 1000ミリ秒 = 1秒
} catch (InterruptedException e) {
// 割り込み時の処理
}
JavaThread.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ミリ秒
JavaTimeUnit を使うメリットは、「単位変換を自分で計算しなくてよい」「コードを読んだときに意図が明確」という点です。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 を使って「秒」「ミリ秒」などの単位をコード上で明示する。
リトライやポーリングなど、「待ちながら繰り返す」処理では、スリープユーティリティを通して意図を分かりやすく書く。
テストでは、必要に応じて「実際には待たないスリーパー」に差し替える設計も検討する。
