markdown

はじめに (対象読者・この記事でわかること)

この記事は、Java の基礎知識はあるものの、Timer クラスや schedule メソッドの実践的な使い方に不安があるプログラマーを対象としています。特に、バックグラウンドで定期的にタスクを走らせたいが、正しいスケジューリング方法や例外処理のベストプラクティスが分からない方に最適です。この記事を読むことで、Timer のインスタンス生成から schedule の各オーバーロードの意味、キャンセルや例外ハンドリングまでを網羅的に理解し、実務で安全にタイマー処理を実装できるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- Java の基本的な文法とオブジェクト指向概念
- スレッドや Runnable インターフェースの基礎知識
- 開発環境(IDE もしくは CLI)でのコンパイルと実行手順

Timer クラスと schedule メソッドの概要

Java 標準ライブラリに含まれる java.util.Timer は、シングルスレッドでタスクをスケジューリングするためのユーティリティです。内部的には TimerThread というデーモンスレッドが動作し、指定された時刻や間隔で TimerTask を実行します。schedule 系のメソッドは、実行タイミングを指定する主要なインターフェースで、次のようなバリエーションがあります。

メソッド 主な用途
schedule(TimerTask task, long delay) 指定遅延後に 1 回だけ実行
schedule(TimerTask task, Date time) 指定日時に 1 回だけ実行
scheduleAtFixedRate(TimerTask task, long delay, long period) 固定レートで繰り返し実行
schedule(TimerTask task, long delay, long period) 前回実行終了時点からの遅延で繰り返し実行

TimerTaskrun() メソッドをオーバーライドしてタスク本体を記述します。ここで注意すべきは、schedule 系は 例外がスローされた場合に Timer スレッドが停止 してしまう点です。そのため、タスク内部で例外処理を適切に行うか、ScheduledExecutorService へ置き換えることも検討すべきです。

実装例と詳しい手順

以下では、典型的な 3 パターンの実装例を示しながら、コードのポイントと落とし穴を解説します。

ステップ1:基本的な 1 回実行タスク

Java
import java.util.Timer; import java.util.TimerTask; public class OneShotExample { public static void main(String[] args) { Timer timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { System.out.println("5 秒後に実行されました: " + System.currentTimeMillis()); } }; // 5 秒 (= 5000 ms) 後に 1 回だけ実行 timer.schedule(task, 5000); } }

ポイント
- Timernew Timer() だけでデーモンスレッドが生成されます。
- schedule(task, delay) の第 2 引数はミリ秒単位です。
- アプリケーションが終了するときは timer.cancel() を呼び出すか、Timer(true) のようにデーモンスレッドに設定しておくと、JVM の終了がブロックされません。

ステップ2:一定間隔での繰り返しタスク(固定レート)

Java
import java.util.Timer; import java.util.TimerTask; public class FixedRateExample { public static void main(String[] args) { Timer timer = new Timer(); TimerTask repeatedTask = new TimerTask() { private int count = 0; @Override public void run() { System.out.println("実行回数: " + (++count) + " 時刻: " + System.currentTimeMillis()); // デモ用に 5 回で停止 if (count >= 5) { timer.cancel(); } } }; // 2 秒遅延後、1 秒ごとに固定レートで実行 timer.scheduleAtFixedRate(repeatedTask, 2000, 1000); } }

ポイント
- scheduleAtFixedRate は「予定時刻」に合わせて実行予定を決めるため、処理が遅れても次回は予定時刻に合わせて実行され、ドリフトが蓄積しません
- cancel() を呼び出すと全タスクが停止し、Timer スレッドも終了します。
- 長時間実行する場合は Timer(true) でデーモンスレッド化し、アプリ終了時にリソースが残らないようにします。

ハマった点やエラー解決

1. IllegalStateException: Timer already cancelled

原因Timer を一度 cancel() した後に、同じインスタンスで schedule 系メソッドを呼び出した。
解決策cancel() 後は新しい Timer インスタンスを生成するか、schedule 前に isCanceled フラグをチェックする。

2. java.lang.IllegalArgumentException: Negative delay.

原因scheduledelay または period に負の数を渡した。
解決策:引数が非負であることを事前に検証し、Math.max(0, value) で補正するか、例外を捕捉して適切に通知。

3. タスク内部の例外で Timer が停止する

原因TimerTask.run() 内で RuntimeException がスローされた。Timer スレッドは例外を捕捉せずに終了するため、以降のタスクは実行されなくなる。
解決策:タスク内部で必ず例外を捕捉し、ログ出力やリトライロジックを組む。例えば:

Java
@Override public void run() { try { // 本来の処理 } catch (Exception e) { e.printStackTrace(); // もしくはロガーへ出力 } }

もしくは、ScheduledExecutorService に置き換えると、例外がスレッドプール全体に影響しにくくなります。

解決策のベストプラクティス

  1. 例外ハンドリングの徹底TimerTask 内で例外を捕捉し、必要ならリトライやアラートを実装。
  2. リソース管理:プログラム終了時は必ず timer.cancel() もしくはデーモン化してリソースリークを防止。
  3. 代替 API の検討:高度なスケジューリングが必要な場合は java.util.concurrent.ScheduledExecutorService を使用すると、スレッドプール管理や例外処理が容易になる。

まとめ

本記事では、Java の Timer クラスと schedule 系メソッドの基本的な使い方から実装例、よくある落とし穴とその対策までを網羅的に解説しました。

  • Timer の生成とタスク登録TimerTimerTask の関係性を把握し、遅延・固定レートの違いを理解する。
  • 例外処理とリソース管理:タスク内部での例外捕捉と cancel() の適切なタイミングを守ることで、Timer が不意に停止するリスクを回避できる。
  • より高度なスケジューリング:必要に応じて ScheduledExecutorService への移行を検討し、スレッドプールや柔軟なキャンセル機構を活用する。

これらを実践することで、バックグラウンド処理や定期的なジョブ実行を安全かつ効率的に実装できるようになります。次回は ScheduledExecutorServiceCompletableFuture を組み合わせた非同期パイプラインの構築方法について解説する予定です。

参考資料