はじめに (対象読者・この記事でわかること)
本記事は、Java を使ってリアルタイムに「毎秒」何らかの値(例: センサーデータ、時刻、カウンタ)を取得し、標準出力やログに出力したいと考えている開発者を対象としています。
- Java の基本的な文法は理解しているが、マルチスレッドやスケジューリングの実装に自信がない方
- 「1 秒ごとに処理を走らせる」典型的なパターンを学び、実務で使えるサンプルコードを手に入れたい方
この記事を読むと、以下ができるようになります。
ScheduledExecutorServiceとTimerの違いと選択基準が分かる- 毎秒実行されるタスクを安全に作成し、例外処理や停止方法を実装できる
- 実際のコード例を元に自分のプロジェクトへすぐに組み込める
このテーマを扱うきっかけは、IoT デバイスやゲームサーバーで「1 秒ごとに状態を更新」する要件が頻繁に出てくるため、シンプルかつ堅牢な実装パターンをまとめたく思ったことにあります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Java SE 8 以降の基本的な文法とクラス設計
- 基本的な例外処理 (
try/catch) の理解 - 標準入出力 (
System.out.println) の使用経験
毎秒値を出力する概要と背景
リアルタイム性が求められるシステムでは、一定間隔での処理が不可欠です。例えば、株価データの取得、ゲームのフレーム更新、IoT デバイスのセンサーデータ収集など、「1 秒ごとに」 行うべきタスクは多岐にわたります。
Java 標準ライブラリはこの要件を満たすために主に二つの API を提供しています。
-
java.util.Timer/TimerTask
- シンプルで軽量。古いバージョンから存在する。
- ただし、単一スレッドでタスクを実行するため、長時間ブロックするタスクがあると次の実行が遅延するリスクがあります。 -
java.util.concurrent.ScheduledExecutorService
-Executorフレームワークの一部として提供され、スレッドプールを利用できる。
- タスクごとにスレッドプールからスレッドを取得するため、ブロックタスクが他のタスクに影響しにくい。
- 高度な例外処理やキャンセルが容易。
本稿では、拡張性と安全性が高い ScheduledExecutorService を中心に解説します。まずは基本的な構成要素と、1 秒ごとの実行スケジュールを設定する流れを示します。
実装手順:ScheduledExecutorService を使った毎秒処理
ステップ 1:Executor の作成と設定
Javaimport java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class PerSecondPrinter { // コアプールサイズは1で十分。必要に応じて増やすことも可能 private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); public static void main(String[] args) { // タスク登録は後述 startPrintingTask(); // デモ用に 10 秒後にシャットダウン scheduler.schedule(() -> { System.out.println("Shutting down scheduler..."); scheduler.shutdown(); }, 10, TimeUnit.SECONDS); } }
Executors.newScheduledThreadPool(int)でスレッドプールを生成。corePoolSizeを 1 にすると、1 つのスレッドで順番にタスクが実行され、CPU 負荷が低く抑えられます。scheduler.shutdown()による正常終了を忘れないようにします。
ステップ 2:毎秒実行するタスクの実装
Javaimport java.time.LocalDateTime; import java.time.format.DateTimeFormatter; private static void startPrintingTask() { Runnable task = () -> { try { // 例として現在時刻を秒単位で出力 String now = LocalDateTime.now() .format(DateTimeFormatter.ofPattern("HH:mm:ss")); System.out.println("Current time: " + now); // ここに実際に取得したい値(例: センサーデータ)を付加 // double sensorValue = readSensor(); // System.out.println("Sensor: " + sensorValue); } catch (Exception e) { // タスク内部で例外が発生した場合、スレッドが死亡しないように捕捉 System.err.println("Error in scheduled task: " + e.getMessage()); e.printStackTrace(); } }; // 初回遅延 0 秒、以降は 1 秒ごとに実行 scheduler.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS); }
ポイント:
scheduleAtFixedRateは「開始時点からの固定間隔」でタスクを実行します。実行時間が 1 秒未満であれば、毎秒ちょうど に呼び出されます。try/catchを入れることで、タスク内で例外が発生してもスレッドが停止せず、以降の実行が継続します。- 実際のデータ取得ロジックは
readSensor()のようなメソッドに切り出すと、テストやリファクタリングが楽になります。
ステップ 3:タスクの停止とリソース解放
上記サンプルでは scheduler.schedule を使い、10 秒後に自動的に shutdown していますが、実運用では以下のように外部から制御するケースが多いです。
Javapublic static void stopPrintingTask() { if (!scheduler.isShutdown()) { scheduler.shutdownNow(); // 実行中タスクを割り込んで停止 try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("Scheduler did not terminate gracefully."); } } catch (InterruptedException ie) { Thread.currentThread().interrupt(); // 再割り込み } } }
shutdownNow()はキューに残っているタスクを即座にキャンセルし、実行中タスクに割り込みを送ります。awaitTerminationで終了を待ち、タイムアウトした場合は警告を出します。
ハマりやすい点とエラー対策
| 現象 | 原因 | 対策 |
|---|---|---|
| タスクが遅延して「1 秒以上の間隔」になる | 前回タスクの処理時間が 1 秒を超えている | scheduleAtFixedRate では間隔は固定されますが、処理時間が長いと次回開始が遅れます。処理時間が不安定な場合は scheduleWithFixedDelay に切り替えるか、タスクを分割して軽量化する |
| 例外が未捕捉でスレッドが死ぬ | Runnable 内で例外が投げられたまま |
try/catch で全例外を捕捉し、ログに出す。また Thread.setDefaultUncaughtExceptionHandler でも全スレッドの例外をハンドリング可能 |
| アプリ終了時にスレッドが残存しプロセスが終了しない | scheduler.shutdown() を呼び忘れた |
アプリの終了フック (Runtime.getRuntime().addShutdownHook) に scheduler.shutdown() を登録して確実にクリーンアップ |
高頻度(1 秒未満)にしたいが ScheduledExecutorService が遅れる |
Java のタイマ精度は OS のスケジューラに依存 | 必要に応じて java.time.Clock と while ループでカスタムスリープを実装するか、リアルタイム OS の利用を検討 |
発展的な応用例
- 複数タスクの同時スケジューリング:スレッドプールサイズを増やし、異なる間隔のタスクを同時に管理。
- 外部設定ファイルで間隔を変更:
PropertiesやYAMLから実行間隔を読込むことで、再デプロイなしに調整可能。 - メトリクス収集:
MicrometerやPrometheusと連携し、タスクの実行回数や処理時間をモニタリング。
まとめ
本記事では、Java において 毎秒単位で値を取得・出力 する典型的な実装パターンを解説しました。
ScheduledExecutorServiceを用いたスケジュール実行は、スレッドプールによる安全性と柔軟な例外処理が特徴です。scheduleAtFixedRateとscheduleWithFixedDelayの使い分け、タスク内での例外捕捉、適切なシャットダウン手順がポイントです。- ハマりやすい点として処理時間が長い場合の遅延や例外によるスレッド停止が挙げられ、対策を示しました。
これらを踏まえて実装すれば、「毎秒確実に」 データを取得し続ける堅牢なシステムを構築できます。次回は同様の手法を用いた 高頻度(ミリ秒単位)タイマー と、分散環境でのスケジューラ共有 について掘り下げる予定です。
参考資料
- Java Platform SE Documentation – ScheduledExecutorService
- Effective Java – 第3章 スレッド安全なクラスの設計 (Joshua Bloch)
- Baeldung – Guide to the Java ScheduledExecutorService
- 「Java Concurrency in Practice」 (Brian Goetz) – スレッドプールとスケジューリングの詳細解説
