CodeGym /コース /JAVA 25 SELF /プロセスのライフサイクル管理

プロセスのライフサイクル管理

JAVA 25 SELF
レベル 61 , レッスン 2
使用可能

1. プロセスの終了待ち: waitFor()

Java から外部プロセスを起動すると、そのプロセスは独自に動作します。何かを計算したり、コンソールに書き込んだり、時にはハングすることもあります(こんにちは、ping の百万パケット)。しかし、時にはそのプロセスがいつ終了したか、どのように終了したか(成功か失敗か)を知る必要があり、— うまくいかなかったときにはそれを「終了」できるようにしたいものです。現実と同じで、誰かに仕事を任せたなら、やり遂げたかを知り、突然コーヒーを延々と淹れ始めたら止められるようにしておくべきです。

ProcessBuilder でプロセスを起動すると、Process 型のオブジェクトが得られます。これはリモコンのようなものです。これを通じて、プロセスの終了を待つ、終了コードを取得する、プロセスを終了する、入出力ストリームを管理する、といった操作ができます。

プロセスの終了をどう待つか?

そのために waitFor() メソッドがあります。

ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "3");
Process process = builder.start();

System.out.println("プロセスの終了を待っています...");
int exitCode = process.waitFor(); // プロセスが終了するまで現在のスレッドをブロックする
System.out.println("プロセスは次のコードで終了しました: " + exitCode);

何が起きるか?

  • プログラムは外部プロセス(ping)を起動します。
  • その後、waitFor() を呼んだスレッドは、外部プロセスが終了するまで「スリープ」します。
  • 外部プロセスが終了すると、waitFor() は終了コードを返します(通常は 0 が成功、0 以外はエラー)。

終了コードを知るには?

このコードは waitFor() が返します。個別に exitValue() で取得することもできます。

int code = process.exitValue();

ただし注意してください。プロセスがまだ終了していない場合、exitValue() の呼び出しは IllegalThreadStateException を投げます。したがって通常は、先に waitFor() で終了を待ってからコードを確認します。

2. プロセスの中断: destroy()destroyForcibly()

外部プロセスが長時間動作したりハングすることがあります。例えば、誰かが ping1000 パケットで起動したのに、あなたの締切は 5 秒後。そんなときは「非常停止ボタン」が必要です。

穏やかな終了: destroy()

destroy() メソッドはプロセスに終了シグナルを送ります(Unix では SIGTERM、Windows では CTRL-BREAK)。プロセスには正しく終了する機会が与えられます。

Process process = new ProcessBuilder("ping", "google.com").start();

Thread.sleep(2000); // 2 秒ほど動かしておく
process.destroy(); // 終了を依頼する

強制終了: destroyForcibly()

プロセスが丁寧なお願いに反応しないときは、destroyForcibly() を使えます。電源プラグを引き抜くようなものです。

process.destroyForcibly();

重要: destroy() または destroyForcibly() を呼んだ後は、waitFor() でプロセスの終了を必ず待ってください。プロセスが自分の終了を「認識」するまで時間が必要な場合があります。

3. タイムアウト付きの待機

Java 8 以降ではタイムアウト付きのメソッドがあります。

boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (finished) {
    System.out.println("プロセスは時間内に終了しました!");
} else {
    System.out.println("プロセスが終了しないため、終了します...");
    process.destroy();
}
  • プロセスが 5 秒以内に終了した場合、メソッドは true を返します。
  • そうでなければ false を返し、対処(たとえばプロセスの終了)ができます。

例: 時間のかかるコマンドを起動し、「停止」してみましょう。

ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "10");
Process process = builder.start();

boolean finished = process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS);
if (!finished) {
    System.out.println("プロセスが長く動作しすぎているため、終了します...");
    process.destroy();
    // 完全に終了するまで待つこともできる
    process.waitFor();
}
System.out.println("終了コード: " + process.exitValue());

4. 実践: タイムアウト付きでコマンドを起動するミニユーティリティ

外部コマンドをタイムアウト付きで起動し、終了を待機し、時間を超えたらプロセスを終了する簡単なプログラムを書きましょう。

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

public class ProcessTimeoutDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        Scanner scanner = new Scanner(System.in);

        System.out.println("実行するコマンドを入力してください(例: ping google.com):");
        String commandLine = scanner.nextLine();
        String[] command = commandLine.split(" ");

        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.start();

        System.out.println("何秒待機しますか?");
        int timeout = scanner.nextInt();

        boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
        if (finished) {
            System.out.println("プロセスは自動的に終了しました。終了コード: " + process.exitValue());
        } else {
            System.out.println("時間切れです!プロセスを終了します...");
            process.destroy();
            process.waitFor();
            System.out.println("プロセスは強制的に終了しました。終了コード: " + process.exitValue());
        }
    }
}

説明:

  • ユーザーが実行するコマンドを入力する。
  • プログラムは指定時間だけ待機する。
  • プロセスが終了しなければ、それを終了する。

豆知識: Windows では ping は既定で 4 回、Linux では無限です。Linux/Mac の場合はパラメータ -c 5 を追加します(例: ping google.com -c 5)。

5. 終了コードの取得: 何で、何のため?

コマンドラインの世界では、コマンドが正常終了した場合は 0 を返し、何か問題があれば別のコードを返すのが慣例です。

  • Unix 系のシステム: 0 = 成功、1 以上 = エラー。
  • Windows もほぼ同じです。

このコードを使って Java プログラム内で判断できます。

int exitCode = process.waitFor();
if (exitCode == 0) {
    System.out.println("成功!");
} else {
    System.out.println("エラー! コード: " + exitCode);
}

6. ハングしたプロセスの終了

プロセスが単にハングすることがあります。たとえば何かの入力を待っていたり、ネットワークに接続できなかったり。放置すると、システム上でぶら下がり続け、リソースを消費します。

これを防ぐには、プロセスに我慢の限界を設定しておくべきです。メソッド waitFor(timeout, unit) により、一定時間のみ終了を待機できます。時間内に終わらなければ、ためらわずに終了させましょう。

destroy() を呼んだ後は、ただ放置するのではなく、本当にプロセスが去ったかを確認することが重要です。そのために再び waitFor() を呼びます。それでもダメな場合の最後の手段が destroyForcibly()。余計なやり取りなしにすべてを中断します。

二段階の終了の例

boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
    process.destroy();
    if (!process.waitFor(2, TimeUnit.SECONDS)) {
        process.destroyForcibly();
    }
}

Process を扱う際の重要ポイント

プロセスが終了したら、すぐに忘れず、その出力 — 標準出力とエラー出力 — を確認しましょう。失敗メッセージや警告、あるいは成功の確認が含まれていることがあります。

その後は必ず関連するすべてのストリーム — InputStreamOutputStreamErrorStream — を閉じてください。退室時に電気を消すようなもので、リソースを節約し、システムを健全に保ちます。

また、プロセスを強制終了した場合は、出力を正しく書き出す前に止まってしまうことがあります。心配はいりません — そのような状況では自然な挙動です。

7. 例: 長時間コマンドの起動と終了

例えば、長時間実行されるプロセス(たとえばパケット数の多い ping)を起動し、3 秒後に終了させたいとします。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;

public class KillLongProcessDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        String[] command = {"ping", "google.com"};
        // Linux/Mac の場合: {"ping", "google.com", "-c", "100"}
        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.start();

        // 別スレッドでプロセスの出力を読む
        Thread readerThread = new Thread(() -> {
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                // 無視する
            }
        });
        readerThread.start();

        // 3 秒待つ
        if (!process.waitFor(3, TimeUnit.SECONDS)) {
            System.out.println("プロセスが長く動作しすぎているため、終了します...");
            process.destroy();
            process.waitFor();
        }
        readerThread.join(); // 出力の読み取り完了を待つ
        System.out.println("プロセスは終了しました。終了コード: " + process.exitValue());
    }
}

8. プロセス管理での典型的なミス

エラー №1: プロセスの終了を待たない。 プロセスを起動してそのまま放置(waitFor() を呼ばない)すると、Java プログラムが終了した後でもプロセスがシステムに残存し続けることがあります。入出力ストリームが閉じられず、メモリやファイルディスクリプタのリークにつながります。

エラー №2: プロセスの不適切な終了。 destroy() を呼んだのに、waitFor() で終了を待たなかった。結果としてプロセスが「ゾンビ」のまま残ることがあります(特に Unix 系で)。

エラー №3: プロセス終了前に終了コードを取得しようとする。 プロセスの終了前に exitValue() を呼ぶと、IllegalThreadStateException がスローされます。

エラー №4: 例外を処理しない。 プロセス関連のメソッドは IOExceptionInterruptedException をスローすることがあります。キャッチするか、メソッドシグネチャに宣言するのを忘れないでください。

エラー №5: プロセスの出力を読まない。 stdoutstderr を読み取らないと、出力バッファがあふれてプロセスがハングすることがあります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION