Javaは言語仕様でマルチスレッドを取り込んでいるため、スレッドを使って周期的に処理を実行させたり、一定時間後に処理を実行させたりすることができます。
マルチスレッドの制御の基本となるのがjava.lang.Threadクラスとjava.lang.Runnableインタフェースです。しかし、これを直接使ってプログラマが一からタイマ機能を記述するのは少々大変です。
そこで、Javaのバージョンがアップする段階でタイマ機能のユーティリティ・クラスが追加されてきました。以下にタイマ機能の種類とそれが追加されたJavaのバージョンを示します。
Javaバージョン | 追加されたタイマ機能 |
---|---|
Java 2 Standard Edition 1.2 | javax.swing.Timer |
Java 2 Standard Edition 1.3 | java.util.Timer |
Java 2 Standard Edition 5.0 | java.util.concurrent.ScheduledExecutorService |
javax.swing.Timer
Swingのクラスライブラリの1つとして提供されている。一定時間後にイベントを実行することができる。これは、Swingのイベントスレッドで実行されるので、描画に関わる処理を記述していてもSwingスレッドと干渉しないで済むのがメリット。実行はSwingのイベントキューに載せられるため、正確性には欠ける。
java.util.Timer
汎用のタイマとして提供されている。処理を周期的に実行したり、一定時間経過後に実行したり、指定した日時に実行したりと多彩な機能を持つ。Swingとは無関係なので、ヘッドレス(GUIが無い環境)でも使用できる。Timer専用スレッドで管理され、正確性もjavax.swing.Timerよりは高い。しかし、java.util.Timerの実装上欠点がいくつかある。まず、スケジューリングにシステムクロックを使用するので、システムクロックの変更に影響を受けてしまう。次に、Timerは一つのスレッドで複数のタスクを実行するのであるタスクが長くスレッドを占有すると他のタスクの周期が不正確になる。また、あるタスクが非チェック例外を投げるとTimerの唯一のスレッドが終了してしまう。
java.util.concurrent.ScheduledExecutorService
Threadより高レベルな並行タスク・フレームワークutil.concurrentパッケージの1つとして提供されている。遅延および周期的なタスク実行をサポートする。java.util.Timerの欠点を補っているので、J2SE 5.0以降のバージョンのJavaを使う場合はこちらを使うのがよい。
java.util.Timerを使って、周期的に実行を行う例を見てみます。
/* * Copyright (C) 2001 by Toru TAKAHASHI, All Rights Reserved. */ package jp.gr.java_conf.torutk.coding.timer; import java.util.Timer; import java.util.TimerTask; /** * UtilTimerTest.java * java.util.Timerの使い方を検証するテストコード。 * 一時停止、再開を行いたい。 * * @author <a href="mailto:torutk@alles.or.jp">Toru TAKAHASHI</a> * @version $Id: UtilTimerTest.java 72 2007-05-09 23:19:38Z toru $ */ public class UtilTimerTest { public UtilTimerTest() { timer = new Timer(true); time = new Time(); } /** * Timerを開始(再開)する。 * Timerに登録するTaskがnullであれば、新規にTaskを生成してから、これを * Timerにセットしている。再開時に前回の状態を引き継ぐため、Taskの生成 * 時にtimeオブジェクトを渡している。 */ public void start() { if (task == null) { task = new TestTask(time); } System.out.println("Taskを開始します"); timer.schedule(task, 0, 1000); } /** * Timerを停止する。 * 実は、Timerに登録したTaskオブジェクトをcancelしてから破棄(=null)して * いる。 */ public void stop() { task.cancel(); task = null; System.out.println("Taskが停止しました"); } /** * Timerのテストプログラムのmainメソッド。 * 10秒毎に、テストの開始、終了を交互に呼び出す。 * * @throws InterruptedException タイミングを取るためにThreadクラスの * ブロッキング・メソッドsleepを呼び出しているが、意図しないインタラプトが * 発生したときにスローする */ public static void main(String[] args) throws InterruptedException { UtilTimerTest tester = new UtilTimerTest(); tester.start(); Thread.sleep(10 * 1000); tester.stop(); Thread.sleep(10 * 1000); tester.start(); Thread.sleep(10 * 1000); tester.stop(); Thread.sleep(10 * 1000); tester.start(); Thread.sleep(10 * 1000); tester.stop(); } /** 一定時間毎にタスクを起動するTimer */ private Timer timer = null; /** Timerから一定時間毎に起動されるタスク */ private TestTask task = null; /** 時刻を保持するオブジェクト */ private Time time = null; } // UtilTimerTest /** * タイマから起動されるタスク。 * 時刻を保持するTimeクラスを持ち、タイマから起動されるごとに、時刻を * 刻むため、Timeメソッドのtick()を呼び出す。 */ class TestTask extends TimerTask { private Time time; public TestTask(Time aTime) { time = aTime; } public void run() { time.tick(); System.out.println("Second = " + time.getSecond()); } } /** * 時刻を保持するクラス。 * サンプル用に秒だけを保持する簡易実装しかしていない。 */ class Time { private int second = 0; public void tick() { second++; } public int getSecond() { return second; } }
UtilTimerTestクラスは、秒だけを刻む簡単なTimeクラスを保持してストップウォッチを実現します。1秒に1回Timeインスタンスの時刻を更新(インクリメント)し、コンソールに表示します。また、startメソッドとstopメソッドを用意しており、ストップウォッチの一時停止状態や再開を実現しています。
このプログラムでは、java.util.Timerに周期的な処理の実行をさせる例と、その周期的な処理の実行を一時停止したり再開したりする方法を紹介しています。
java.util.Timerクラスのインスタンスを生成します。
public UtilTimerTest() { timer = new Timer(true); time = new Time(); }
引数にtrueを設定すると、このTimerに割付けられた処理(タスク)はデーモンスレッドによって実行されます。デーモンスレッドは、他の有効なスレッドが全て終了していると終了します。よって、このプログラムではmainメソッドが終了するときにTimerスレッドも終了します。
falseまたは何も指定せずにTimerインスタンスを生成すると、mainメソッドが終了してもタスクを実行し続けます。
Timerを使って周期的あるいは一定時間後に実行させたい処理は、java.util.TimerTask抽象クラスを拡張(extends)して記述します。
class TestTask extends TimerTask { private Time time; public TestTask(Time aTime) { time = aTime; } public void run() { time.tick(); System.out.println("Second = " + time.getSecond()); } }
Timerからは、runメソッドが呼ばれるので、ここに処理を記述します。このあたりは、java.lang.Threadを使う場合と同じですが、Threadの場合と違って周期的あるいは一定時間の経過判定を管理する必要がない点でプログラミングが楽になります。
Timerにタスクを設定して周期的あるいは一定時間後にタスクを実行させるために、Timerのメソッドを呼び出します。タスクを実行するタイミングは大きく3種類に分かれます。それぞれ用途に合わせてTimerクラスのメソッドを使い分けます。
scheduleメソッド(1)
一定時間経過後または指定した時刻に1回だけタスクを実行させます。例えば、タイムアウト処理や、指定した時刻に処理を起動したいときなどに使います。
scheduleメソッド(2)
周期的にタスクを実行させます。何らかの原因で実行に遅れが生じても、その次の実行までの周期(間隔)は指定された値を使用します。例えば、アニメーションの画面更新処理なんかに使います。
scheduleAtFixedRateメソッド
周期的にタスクを実行させます。何らかの原因で実行に遅れが生じると、その次以降のタスクの実行を続けて行い、遅れを取り戻すようにスケジューリングします。例えば、時計を作ったり、ある期間に一定回数の処理をさせたいときに使います。
サンプルプログラムでは、2番目のscheduleメソッドを使って1秒間隔でタスクを実行するように設定しています。
timer.schedule(task, 0, 1000);
引数の1番目はTimerで実行する処理を持つTimerTaskオブジェクトを指定します。2番目は、scheduleメソッド呼び出し後に周期的実行するまでの準備期間を指定します。ここではただちにタスクを実行させるため、0[ms]を指定しています。3番目は、タスクを実行する周期を指定します。ここでは、1秒間隔でタスクを実行するように、1000[ms]を指定しています。
なお、今回のサンプルプログラムはストップウォッチを実現しているので、本来はscheduleAtFixedRateメソッドを使うべきでした。
Timerを使って周期的に処理を実行させているときに、一時的に処理の実行を止めて、また後程処理を再開させたいことがあります。Java 2 SDKのAPIドキュメントを読んでも具体的な方法が載っていないので、サンプルプログラムでは自己流で実現しています。
まず、APIドキュメントを見て思い付いた2つの方法を述べます(これらのやり方ではNG)。
まず、Timerのcancelメソッドを起動すると、2度とそのTimerオブジェクトは使用できません。新たにTimerオブジェクトを生成しなくてはなりません。しかも、Timerをcancelした時にTimerに設定されていたTimerTaskオブジェクトも再度利用することはできません。TimerTaskオブジェクトも新たに生成し直す必要があります。
次に、TimerTaskのcancelメソッドを起動すると、2度とそのTimerTaskオブジェクトは使用できません。新たにTimerTaskオブジェクトを生成しなくてはなりません。
そこで、先の2番目のやり方に補正を加え、一時停止する毎にTimerTaskオブジェクトをcancelメソッドを起動してから破棄し(1)、再開するときには新たにTimerTaskオブジェクトを生成することにします。
ただし、再開するときには前のデータを引き継ぐ必要があるので、TimerTaskがデータを保持するように設計し、生成するときにコンストラクタの引数でデータを渡しています。
public void stop() { task.cancel(); task = null; System.out.println("Taskが停止しました"); }
public void start() { if (task == null) { task = new TestTask(time); } System.out.println("Taskを開始します"); timer.schedule(task, 0, 1000); }
java.util.concurrent.ScheduledExecutorServiceを使って、周期的に実行する例を見てみます。前の章のUtilTimerTestと同じ機能を、java.util.Timerからjava.util.concurrent.ScheduledExecutorServiceに置き換えたものとなります。
/* * Copyright (C) 2007 by Toru TAKAHASHI, All Rights Reserved. */ package jp.gr.java_conf.torutk.coding.timer; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.ScheduledFuture; /** * ScheduledExecutorServiceTestクラスは、ScheduledExecutorServiceの使い方を * 検証するテストコードです。 * 一秒毎に時を刻むタスクと、それを一時停止・再開するロジックを記述しています。 * * @author Toru Takahashi * @version $Id: ScheduledExecutorServiceTest.java 106 2007-10-16 14:43:44Z toru $ */ public class ScheduledExecutorServiceTest { public ScheduledExecutorServiceTest() { task = new TestTask(new Time()); scheduler = Executors.newSingleThreadScheduledExecutor(); } /** * タスクを開始(再開)する。 * タスクを固定周期(1秒)で実行します。 */ public void start() { System.out.println("> start called."); future = scheduler.scheduleAtFixedRate( task, 0, 1000, TimeUnit.MILLISECONDS ); } /** * schedulerに登録したタスクを停止する。 */ public void stop() { System.out.println("> stop called."); if (future != null) { future.cancel(true); } } /** * schedulerをシャットダウンする */ public void shutdown() { System.out.println("> shutdown called."); scheduler.shutdownNow(); } /** * テストプログラムのmainメソッド。 * 10秒毎に、テストの開始、終了を交互に呼び出す。 * * @throws InterruptedException タイミングを取るためにThreadクラスの * ブロッキング・メソッドsleepを呼び出しているが、意図しないインタラプトが * 発生したときにスローする */ public static final void main(final String[] args) throws InterruptedException { ScheduledExecutorServiceTest tester = new ScheduledExecutorServiceTest(); tester.start(); Thread.sleep(10 * 1000); tester.stop(); Thread.sleep(10 * 1000); tester.start(); Thread.sleep(10 * 1000); tester.stop(); Thread.sleep(10 * 1000); tester.start(); Thread.sleep(10 * 1000); tester.stop(); Thread.sleep(10 * 1000); tester.shutdown(); } private Runnable task; private ScheduledExecutorService scheduler; private ScheduledFuture<?> future; } /** * ExecutorServiceから起動されるタスク。 * 時刻を保持するTimeクラスを持ち、タイマから起動されるごとに、時刻を * 刻むため、Timeメソッドのtick()を呼び出す。 */ class TestTask implements Runnable { TestTask(Time aTime) { time = aTime; } public void run() { time.tick(); System.out.println("Second = " + time.getSecond()); } private Time time; }
秒を刻むTimeクラスは前の章のUtilTimerと同じものを使用します。ScheduledExecutorServiceから1秒毎にTestTaskのrunメソッドを呼び出しています。TestTaskはRunnableを実装し、runメソッド内でTimeオブジェクトのtickメソッドを1回呼びます。
scheduler = Executors.newSingleThreadScheduledExecutor();
concurrentパッケージでは、Executorsクラスのstaticなファクトリ・メソッドを使って幾つかの種類のExecutorを生成します。ScheduledExecutorを生成するファクトリ・メソッドには、スレッド数を任意に指定可能なnewScheduledThreadPoolメソッドと単一スレッドのnewSingleThreadExecutorがあります。ここでは、newSingleThreadScheduledExecutorを使用しています。
ScheduledExecutorServiceに登録する処理は、Runnableインタフェースを実装している必要があります。
class TestTask implements Runnable { TestTask(Time aTime) { time = aTime; } public void run() { time.tick(); System.out.println("Second = " + time.getSecond()); } private Time time; }
ScheduledExecutorServiceにタスクを設定して周期的あるいは一定時間後にタスクを実行させるために、ScheduledExecutorServiceのメソッドを呼び出します。タスクの起動タイミングによって以下のメソッドを使い分けます。
scheduleメソッド
指定した時間が経過後に一回だけ起動するタスクを指定します。
scheduleAtFixedRateメソッド
指定した時間が経過後に指定した周期で起動するタスクを指定します。
scheduleWithFixedDelayメソッド
指定した時間が経過後に指定した間隔を空けて起動するタスクを指定します。
本サンプルプログラムでは、2番目のscheduleAtFixedRateメソッドを使って1秒間隔でタスクを実行するように設定しています。
future = scheduler.scheduleAtFixedRate(task, 0, 1000, TimeUnit.MILLISECONDS);
引数の1番目はRunnableインタフェースを実装したオブジェクトを指定します。
2番目は、scheduleAtFixedRateメソッド呼び出し後から最初のタスクを実行するまでの準備期間を指定します。ここでは、0[ms]を指定しています。
3番目は、タスクを実行する周期を指定します。ここでは、1000[ms]を指定しています。
4番目は、引数(2番目と3番目)で指定している値の単位を示す列挙型TimeUnitの値を指定します。ここではTimeUnit.MILLISECONDSを指定しています。
戻り値は、登録したタスクを表わすScheduledFutureインタフェース実装オブジェクトです。ここでは後程タスクの実行を一時停止する際使用するためフィールドに保持しています。
周期的に処理を実行させているときに、一時的に処理の実行を止めて、また後程再開させたいことがあります。ScheduledExecutorServiceにタスクを登録するときに、ScheduledFutureオブジェクトが戻り値として取得できますが、このFutureオブジェクトはcancelメソッドを持っているので、一時停止するときはこのcancelメソッドを呼びます。
public void stop() { System.out.println("> stop called."); if (future != null) { future.cancel(true); } }
cancelメソッドの引数boolean型は、cancelメソッド呼び出し時にちょうど実行中のタスクが存在したときに、タスク(スレッド)に対して割り込み(interrupt)をかけるか(trueのとき)、タスクの終了を待つか(false)の違いがあります。
このサンプルコードはタスクの実行がほぼ瞬時に終わるので違いはありませんが、時間のかかる処理の場合、違いが生じます。なお、引数trueでcancelを呼び出す場合、タスクの実装はinterruptを考慮した実装にする必要があります。
再開するときは、再度ScheduledExecutorServiceにタスクを登録するだけです。
ScheduledExecutorServiceが内部で保持するスレッドを終了させるには、shutdownメソッド(またはshutdownNow)を呼び出します。
public void shutdown() { System.out.println("> shutdown called."); scheduler.shutdownNow(); }