1. 读取/写入流时的死锁
最恼人的错误之一——进程挂起且无法结束,看起来一切都做对了却卡住。常见原因是外部进程的输出或错误流缓冲区被写满。如果不同时读取两个流(stdout 和 stderr),进程在尝试输出时可能会“阻塞”,因为没有人从它的缓冲区取走数据。
问题示例
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 上,命令行工具常用 CP866 或 Windows-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. 资源泄漏
如果不关闭进程的各个流(InputStream、OutputStream、ErrorStream),它们可能一直“挂着”,占用资源,甚至阻碍进程结束。同样地,如果不结束进程本身,它可能会在系统中变成“僵尸”。
错误示例
Process process = builder.start();
// ... 有操作,但没有关闭流!
正确做法
对流使用 try-with-resources:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// 读取输出
}
在完成与进程的交互后,正确停止它:
process.destroy(); // 终止进程(如果它仍在运行)
注意:如果不关闭流,可能会导致内存泄漏、卡死以及系统层面的稳定性问题(尤其是在大量启动进程的场景下)。
6. 交互式通信中的死锁
在交互式通信中很容易出现相互阻塞:Java 等待回应,而外部进程等待你的输入,结果双方都“沉默”。或者你发送了数据却不读取回应——外部程序的缓冲区在某个时刻会被写满,从而停止写入。
为避免这种情况,请分离职责:一个线程负责读取,另一个线程负责写入。这样交互就能并行进行,互不阻塞。同时,在结束后不要留下打开的流——请关闭它们,避免系统持有不必要的资源。
GO TO FULL VERSION