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()
有时外部进程会运行太久或直接卡死。比如有人启动了对 ping 的 1000 个包,而你的截止时间只有 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. 管理进程的常见错误
错误 #1:没有等待进程结束。 如果你启动了进程却把它忘了(没有调用 waitFor()),那么即使你的 Java 程序结束,进程仍可能“挂”在系统里。输入/输出流可能未关闭,从而导致内存和文件描述符泄漏。
错误 #2:不正确地终止进程。 调用了 destroy(),却没有通过 waitFor() 等待它真正结束。结果进程可能变成“僵尸”(尤其在 Unix 系统中)。
错误 #3:在进程结束前就去获取退出码。 在进程结束前调用 exitValue() 会导致抛出 IllegalThreadStateException。
错误 #4:未处理异常。 与进程交互的方法可能抛出 IOException 和 InterruptedException。别忘了捕获它们,或在方法签名中声明。
错误 #5:不读取进程输出。 如果不读取 stdout 和 stderr,进程可能因为输出缓冲区被填满而卡住。
GO TO FULL VERSION