Java | Java 標準ライブラリ:Timer / TimerTask

Java Java
スポンサーリンク

Timer / TimerTask を一言でいうと

TimerTimerTask は、

「指定した時間後に、あるいは一定間隔で、“別スレッドで” 処理を実行してくれるシンプルなタイマー機能」

です。

  • 「5 秒後にこの処理を実行したい」
  • 「1 分ごとにログを取る処理を回したい」

みたいなときに使えます。

Timer は「タイマー本体(スケジューラ)」、
TimerTask は「実際に実行したい処理(タスク)」
という役割分担です。


基本イメージ:タイマーに「タスク」を登録しておく

TimerTask は「実行したいこと」を書くクラス

まず、「何をやりたいか」を TimerTask に書きます。

import java.util.TimerTask;

public class HelloTask extends TimerTask {
    @Override
    public void run() {
        System.out.println("タイマーからの実行: " + System.currentTimeMillis());
    }
}
Java

TimerTaskrun() メソッドを 1 つ持つクラスで、
その中に「タイマー発火時に実行したい処理」を書きます。

これはイメージ的に、Thread に渡す Runnable に似ています。
「何をするか」だけをカプセル化している存在です。

Timer は「いつ実行するか」を決めるクラス

次に、その TimerTask をいつ実行するかを Timer に指示します。

import java.util.Timer;

public class TimerBasic {
    public static void main(String[] args) {
        Timer timer = new Timer();           // タイマー本体
        TimerTask task = new HelloTask();    // 実行したい処理

        timer.schedule(task, 3000L);         // 3秒後に1回だけ実行

        System.out.println("main は終わりそうに見えるけど…");

        try {
            Thread.sleep(5000L);             // 5秒待って、タイマーの動きを見届ける
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        timer.cancel();                      // 使い終わったらキャンセル(後で詳しく説明)
    }
}
Java

timer.schedule(task, 3000L);
これで「今から 3 秒後に task.run() を別スレッドで実行してね」という予約をします。

ここで大事なのは、

Timer が内部で専用スレッドを持ち、そのスレッド上で TimerTask#run() が呼ばれる
main スレッドとは別に動く

という点です。


一回だけか、繰り返しか:schedule と scheduleAtFixedRate

一回だけ実行:schedule(TimerTask task, long delay)

まずは一番シンプルな「一回だけ」のケースです。

timer.schedule(task, delayMillis);
Java

delayMillis ミリ秒後に、タスクが一度だけ実行されます。

例えば「10 秒後にメッセージを出したい」ならこうです。

timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("10秒経ちました");
    }
}, 10_000L);
Java

ここでは無名クラスで TimerTask をその場で定義しています。
Java 8 以降なら、TimerTask は抽象クラスなのでラムダは直接は使えませんが、
new TimerTask() { public void run() { ... } } で十分です。

一定間隔で実行:schedule(TimerTask task, long delay, long period)

「一定間隔で繰り返したい」場合は、第三引数に period を渡します。

timer.schedule(task, delayMillis, periodMillis);
Java

例えば、「5 秒後に初回を実行し、その後は 2 秒ごとに繰り返す」ならこうです。

Timer timer = new Timer();

timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("繰り返し実行: " + System.currentTimeMillis());
    }
}, 5_000L, 2_000L);
Java

ここでの挙動は、

開始から 5 秒後に 1 回目
以降は毎回「前回の実行完了時刻」から 2 秒後を目安に実行

というイメージです(多少のズレはあり得ます)。

scheduleAtFixedRate との違い(ここは少し重要)

Timer には、schedule の他に scheduleAtFixedRate というメソッドもあります。

timer.scheduleAtFixedRate(task, delayMillis, periodMillis);
Java

違いを直感的に言うと、

schedule
→ 「前回終わってから period だけ待つ」

scheduleAtFixedRate
→ 「理想的なスケジュールに合わせようとして、遅れていたら巻き返す」

です。

少し具体的にします。

  • period = 1000ms(1 秒)
  • タスクの実行時間が 200ms だったとします。

schedule の場合:
1回目終了 → そこから 1000ms 待って 2回目実行
という形なので、実行周期は「約 1.2 秒」に近づいていきます。

scheduleAtFixedRate の場合:
理想的には「0ms, 1000ms, 2000ms, 3000ms …」というタイミングで実行したい
多少遅れても、その理想に合わせようとする

つまり、「正確な周期性」が重要なら scheduleAtFixedRate を、
「1 回ずつ間を空けられればいい」なら schedule を使う、というイメージです。

初心者のうちは、まず schedule(task, delay, period) に慣れ、
「周期を厳密にしたいケース」が出てきたら scheduleAtFixedRate を検討する、くらいで十分です。


Timer のスレッドとキャンセルの考え方

Timer は内部に「1 本のスレッド」を持つ

new Timer() をすると、
内部でタイマー専用のスレッドが 1 本立ちます。

このスレッドは、予約されたタスクの実行時間を見ながら、
run() を順次呼び出していきます。

ここで押さえておきたいのは、

ひとつの Timer につき、スレッドは原則 1 本だけ
そのスレッドの上で、タスクは「順番に」実行される

という点です。

もし、

ある TimerTask の run() がとても時間のかかる処理だった場合、
後ろのタスクの実行がどんどん遅れていく

ということが起き得ます。

長時間かかる処理やブロッキング I/O を TimerTask の中に書く場合は、
さらに別スレッドに投げたり、ScheduledExecutorService に切り替えたりすることを検討すべきです
ScheduledExecutorService は Timer の後継のような存在です。これは重要ですが、ここでは Timer に話を絞ります)。

Timer の cancel() を必ず意識する

Timer はスレッドを持っているので、
使い終わったら timer.cancel() で止めてあげる必要があります。

Timer timer = new Timer();
// 何かタスクをスケジュール…

// もう使わないと決めたタイミングで
timer.cancel();
Java

cancel() を呼ぶと、

今後スケジュールされているタスクはキャンセル
タイマーのスレッドも終了方向へ

という挙動になります。

小さなサンプルプログラムなら、プロセス終了とともにスレッドも終わるので問題が表面化しにくいですが、
長く動くアプリ(サーバーなど)では、
Timer を作りっぱなしで cancel し忘れると「スレッドリーク」の原因になります。

「Timer を new したら、どこで cancel するか」をセットで設計する癖をつけておいてください。

デーモンスレッドとして作るかどうか

Timer には「デーモンスレッドとして動かす」コンストラクタもあります。

Timer timer = new Timer(true); // true でデーモンスレッド
Java

デーモンスレッドとは、 「ユーザー側のスレッド(非デーモン)が全部終わったら、一緒に終わる」スレッドです。

普通は new Timer()(非デーモン)で構いませんが、
「アプリ終了を邪魔してほしくない」バックグラウンド処理なら、
デーモンモードを使うこともあります。


実用っぽい例題:定期的なログ出力や監視

例1:10 秒ごとにメモリ使用量をログに出す

import java.util.Timer;
import java.util.TimerTask;

public class MemoryLogger {
    public static void main(String[] args) {
        Timer timer = new Timer(true); // デーモンスレッドにしておく

        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                long used = Runtime.getRuntime().totalMemory()
                          - Runtime.getRuntime().freeMemory();
                System.out.println("Used memory: " + used);
            }
        };

        timer.scheduleAtFixedRate(task, 0L, 10_000L); // すぐに開始し、10秒ごと

        try {
            Thread.sleep(60_000L); // 1分ほど動きを観察
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        timer.cancel();
    }
}
Java

このコードでは、

アプリ起動直後に 1 回実行
その後は 10 秒ごとに実行

という形で、メモリ使用量をログに出し続けます。

ここで、「タスク自体は軽い処理なので、Timer の単一スレッドでも十分」という判断になっています。

例2:締め切りタイマー(一定時間で強制的に何かする)

ユーザーが一定時間操作しなかったら、
「セッションタイムアウト」としてログアウトさせるようなイメージです。

import java.util.Timer;
import java.util.TimerTask;

public class SessionTimeout {
    public static void main(String[] args) {
        Timer timer = new Timer();

        TimerTask timeoutTask = new TimerTask() {
            @Override
            public void run() {
                System.out.println("セッションタイムアウト。ログアウトします。");
                // 実際にはここでログアウト処理など
            }
        };

        timer.schedule(timeoutTask, 30_000L); // 30秒後に一回だけタイムアウト

        // もし途中で「ユーザーが操作したら」、タイマーをキャンセルして再設定するイメージ
        // timeoutTask.cancel(); など
    }
}
Java

実際のアプリでは、ユーザー操作のたびに
「古い TimerTask をキャンセル → 新しい TimerTask をスケジュール」という管理をします。

ここから話は「スレッドセーフな状態管理」や「Executor を使った実装」に発展していきますが、
まずは「時間指定で処理を動かす」感覚を掴むことが大事です。


Timer / TimerTask を使うときの注意と、もう一歩先の選択肢

Timer は「古いがまだ使える」クラス

Timer / TimerTask は Java 1.3 時代からある古い API です。

シンプルな割りに便利ですが、
次のような弱点があります。

  • スレッドが 1 本だけなので、重いタスクで詰まりやすい
  • 例外がタスクから外に投げられると、その Timer のスレッドが死んでしまう
  • キャンセル処理やエラー処理を丁寧に書かないと、管理が難しくなる

そのため、最近の Java では、同じことをやるなら

ScheduledExecutorServiceExecutors.newSingleThreadScheduledExecutor() など)

を使う方が推奨されることが多いです。

ただ、学習の入り口として Timer / TimerTask で

「時間を指定して別スレッドで処理を動かす」
「繰り返しのスケジューリングをする」

感覚を掴んでおくのは、とても意味があります。

初心者が特に意識したいポイント

TimerTask#run() は別スレッドで実行されるので、
共有変数に触るときはスレッドセーフを意識する
(GUI アプリなら Swing の EDT や JavaFX の UI スレッドに戻す必要あり)

重い処理をそのまま TimerTask に書くと、後続が遅れる
→ 短く終わる処理にとどめるか、さらに別スレッドや Executor に委譲する

使い終わったら timer.cancel() する
→ 特に長く動くアプリでは重要

このあたりを頭に入れておくだけで、「Timer でのありがちな事故」はかなり防げます。


まとめ:Timer / TimerTask を自分の中でこう位置づける

Timer / TimerTask を初心者向けにまとめると、

「別スレッドで、一定時間後や一定間隔で処理を実行してくれる、シンプルなスケジューラ」

です。

ポイントは、

  • 実行したい処理は TimerTask#run() に書く
  • Timer に対して schedule / scheduleAtFixedRate で「いつ・どのように実行するか」を指示する
  • 内部で専用スレッドが 1 本動いているので、長時間かかる処理を詰め込まない
  • 使い終わったら timer.cancel() で止める

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