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 的注意事項
當行程結束後,別急著忘記它。看一下它的輸出——包括標準輸出與錯誤輸出。那裡可能藏著重要資訊:錯誤訊息、警告,或只是成功完成的確認。
之後務必關閉所有相關串流——InputStream、OutputStream 與 ErrorStream。這就像離開時關燈:節省資源,也讓系統維持整潔。
也請記得:如果你是強制停止行程,它可能來不及正確寫出輸出。這不打緊——在此類情境中屬於預期行為。
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。
錯誤四:未處理例外。 與行程相關的方法可能會丟出 IOException 與 InterruptedException。別忘了攔截它們,或在方法簽名中宣告。
錯誤五:不讀取行程輸出。 如果不讀取 stdout 與 stderr,行程可能因輸出緩衝區滿而卡住。
GO TO FULL VERSION