CodeGym /课程 /JAVA 25 SELF /处理进程时的常见错误解析

处理进程时的常见错误解析

JAVA 25 SELF
第 61 级 , 课程 4
可用

1. 读取/写入流时的死锁

最恼人的错误之一——进程挂起且无法结束,看起来一切都做对了却卡住。常见原因是外部进程的输出或错误流缓冲区被写满。如果不同时读取两个流(stdoutstderr),进程在尝试输出时可能会“阻塞”,因为没有人从它的缓冲区取走数据。

问题示例

ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// 只读取 stdout,忽略 stderr!
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}
process.waitFor();

如果命令往 stderr 写数据(例如,java -version 几乎总是写到那里!),该流的缓冲区会被写满,进程就会挂起。

正确做法

并行读取两个流(使用单独的线程或 ExecutorService):

ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();

// 读取 stdout
Thread stdoutThread = new Thread(() -> {
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println("[stdout] " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

// 读取 stderr
Thread stderrThread = new Thread(() -> {
    try (BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getErrorStream()))) {
        String line;
        while ((line = reader.readLine()) != null) {
            System.err.println("[stderr] " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

stdoutThread.start();
stderrThread.start();

process.waitFor();
stdoutThread.join();
stderrThread.join();

结论:
如果不读取 stderr — 进程可能会挂起。
如果不读取 stdout — 也可能会挂起。
同时读取两个流 — 问题自然消失!

2. 编码问题

外部进程输出文本所用的编码,可能与 Java 程序的默认编码不同。如果不指定正确的编码,会出现乱码(在包含西里尔字符的文本中尤为明显)。

错误示例

BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));

这段代码使用系统默认编码。但如果外部进程按 UTF-8 输出,而你的系统是 Windows-1251,西里尔字符就会变成乱码。

正确做法

如果你知道所需编码,请显式传入:

BufferedReader reader = new BufferedReader(
    new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);

如果不确定,请查阅程序文档或尝试几个备选编码。

小技巧:在 Windows 上,命令行工具常用 CP866Windows-1251,而在 Linux 上通常是 UTF-8

3. 平台差异

在一种操作系统上可用的命令,可能在另一种上不存在。比如 ls 在 Linux/Mac 上可用,但在 Windows 上不可用(对应的是 dir)。命令语法、路径分隔符以及引号也各不相同。

错误示例

ProcessBuilder builder = new ProcessBuilder("ls", "-l");
builder.start(); // 在 Windows 上: "ls" 不存在!

正确做法

String os = System.getProperty("os.name").toLowerCase();
ProcessBuilder builder;
if (os.contains("win")) {
    builder = new ProcessBuilder("cmd", "/c", "dir");
} else {
    builder = new ProcessBuilder("ls", "-l");
}

文件路径:请使用 File.separator 而不是 “/” 或 “\”,以避免路径相关的问题。

4. 权限问题

某些命令需要管理员权限(删除系统文件、修改网络设置等)。如果权限不足,命令会以错误结束,甚至无法启动。

示例

ProcessBuilder builder = new ProcessBuilder("rm", "-rf", "/root/secret.txt");
Process process = builder.start();
// ... 预期会出现错误 Permission denied

正确做法

  • 检查你的命令是否需要提升权限。
  • 通过 process.exitValue() 处理进程的返回码。
  • 读取 stderr——错误原因通常在其中(例如,“Permission denied”)。

5. 资源泄漏

如果不关闭进程的各个流(InputStreamOutputStreamErrorStream),它们可能一直“挂着”,占用资源,甚至阻碍进程结束。同样地,如果不结束进程本身,它可能会在系统中变成“僵尸”。

错误示例

Process process = builder.start();
// ... 有操作,但没有关闭流!

正确做法

对流使用 try-with-resources:

try (BufferedReader reader = new BufferedReader(
         new InputStreamReader(process.getInputStream()))) {
    // 读取输出
}

在完成与进程的交互后,正确停止它:

process.destroy(); // 终止进程(如果它仍在运行)

注意:如果不关闭流,可能会导致内存泄漏、卡死以及系统层面的稳定性问题(尤其是在大量启动进程的场景下)。

6. 交互式通信中的死锁

在交互式通信中很容易出现相互阻塞:Java 等待回应,而外部进程等待你的输入,结果双方都“沉默”。或者你发送了数据却不读取回应——外部程序的缓冲区在某个时刻会被写满,从而停止写入。

为避免这种情况,请分离职责:一个线程负责读取,另一个线程负责写入。这样交互就能并行进行,互不阻塞。同时,在结束后不要留下打开的流——请关闭它们,避免系统持有不必要的资源。

1
调查/小测验
和进程打交道第 61 级,课程 4
不可用
和进程打交道
和进程打交道
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION