CodeGym /課程 /JAVA 25 SELF /行程生命週期管理

行程生命週期管理

JAVA 25 SELF
等級 61 , 課堂 2
開放

1. 等待行程結束:waitFor()

當你從 Java 啟動外部行程時,它自顧自地運作:做計算、往主控台輸出,有時還會卡住(向一次送出百萬封包的 ping 致意)。但有時我們需要知道這個行程何時結束、如何結束(成功或失敗),而且——一旦出問題——要能夠「殺掉」它。就像現實生活:把任務交給別人,最好知道他是否完成,必要時也要能把他叫停,免得他突然開始無止盡地煮咖啡。

當你透過 ProcessBuilder 啟動行程時,會得到一個 Process 物件。這個物件就像遙控器:可以等待行程結束、取得結束碼、終止行程,還能管理 I/O 串流。

如何等待行程結束?

可以使用方法 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()

有時外部行程會跑太久或卡死。比如有人啟動了針對 1000 個封包的 ping,而你的截止時間在 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. 管理行程的常見錯誤

錯誤一:未等待行程結束。 如果你啟動行程後就把它丟著不管(沒有呼叫 waitFor()),那行程甚至可能在你的 Java 程式結束後仍掛在系統上。I/O 串流也可能未被關閉,導致記憶體與檔案描述元洩漏。

錯誤二:不正確的終止方式。 呼叫了 destroy(),但沒有透過 waitFor() 等待行程真正結束。結果行程可能變成「殭屍」(尤其在 Unix 系統)。

錯誤三:在行程結束前就讀取結束碼。 在行程尚未結束時呼叫 exitValue() 會導致例外 IllegalThreadStateException

錯誤四:未處理例外。 與行程相關的方法可能會丟出 IOExceptionInterruptedException。別忘了攔截它們,或在方法簽名中宣告。

錯誤五:不讀取行程輸出。 如果不讀取 stdoutstderr,行程可能因輸出緩衝區滿而卡住。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION