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