处理其输出(包括标准输出 stdout 和标准错误 stderr)是至关重要的,主要有以下几个原因:

- 获取结果:许多命令的执行结果都输出到标准输出中。
- 错误诊断:标准错误流用于输出错误信息、警告或调试信息。
stderr缓冲区满了,外部进程可能会阻塞,导致你的 Java 程序也挂起。 - 防止死锁:这是最关键也最容易出错的一点,如果子进程产生了大量输出,而你的 Java 程序没有及时读取,子进程会等待操作系统缓冲区有空间,从而进入阻塞状态,如果父进程也在等待子进程结束,那么就会形成“死锁”。
下面我将从不同层面和场景,为你详细讲解如何处理 Java Process 的输出。
核心概念:标准流
一个进程通常有三个标准流:
- 标准输入:
InputStream,用于向进程输入数据。 - 标准输出:
OutputStream,进程通过它输出正常信息。 - 标准错误:
OutputStream,进程通过它输出错误信息。
在 Java 中,Process 对象提供了获取这些流的方法:
InputStream getInputStream():获取子进程的标准输出。InputStream getErrorStream():获取子进程的标准错误。OutputStream getOutputStream():获取子进程的标准输入。
基础用法与陷阱(不推荐的方式)
初学者可能会写出如下代码,这种方式在大多数情况下是错误的,因为它极易导致死锁。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class BadProcessExample {
public static void main(String[] args) {
try {
// 启动一个进程,"ping" 命令,它会持续输出
Process process = Runtime.getRuntime().exec("ping -c 5 google.com");
// 错误示范:先等待进程结束,再读取输出
int exitCode = process.waitFor(); // 阻塞在这里,等待进程结束
// 只有在进程结束后,才能读取输出
System.out.println("Process exited with code: " + exitCode);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[STDOUT] " + line);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
为什么这个例子是错的?
假设 ping 命令产生了大量的输出,超出了操作系统的输出缓冲区大小。ping 进程会尝试写入 stdout,但缓冲区已满,ping 进程会阻塞,等待 Java 程序读取缓冲区以释放空间。
你的 Java 程序正在 process.waitFor() 处阻塞,等待 ping 进程结束,这就形成了一个死锁:
ping进程在等待 Java 读取输出。- Java 进程在等待
ping进程结束。
正确的输出处理方式
为了正确处理输出,必须遵循一个核心原则:在等待进程结束之前,必须启动独立的线程来持续读取 stdout 和 stderr。

使用 ProcessBuilder 和 Thread(经典可靠)
这是最经典、最可控的方式,适用于任何 Java 版本,我们为 stdout 和 stderr 分别创建一个线程来读取。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class GoodProcessExample {
// 用于读取输入流的辅助类
private static class StreamGobbler implements Runnable {
private final InputStream inputStream;
private final String type;
public StreamGobbler(InputStream inputStream, String type) {
this.inputStream = inputStream;
this.type = type;
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
// 根据类型打印到不同的地方,例如控制台或日志文件
System.out.println("[" + type + "] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
try {
// 推荐使用 ProcessBuilder
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "5", "google.com");
pb.redirectErrorStream(false); // false 表示分别处理 stdout 和 stderr
Process process = pb.start();
// 创建并启动线程来读取标准输出
Thread stdoutThread = new Thread(new StreamGobbler(process.getInputStream(), "STDOUT"));
// 创建并启动线程来读取标准错误
Thread stderrThread = new Thread(new StreamGobbler(process.getErrorStream(), "STDERR"));
stdoutThread.start();
stderrThread.start();
// 等待进程执行完成
int exitCode = process.waitFor();
System.out.println("\nProcess finished with exit code: " + exitCode);
// 等待输出读取线程也结束(确保所有输出都被处理)
stdoutThread.join();
stderrThread.join();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
优点:
- 可靠:彻底避免了死锁问题。
- 灵活:可以分别处理
stdout和stderr,例如将错误信息记录到日志文件,而将正常信息打印到控制台。 - 兼容性好:适用于所有 Java 版本。
合并 stdout 和 stderr(简化处理)
如果你不关心 stdout 和 stderr 的来源,希望将它们混合在一起处理,可以使用 ProcessBuilder.redirectErrorStream(true)。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class MergedStreamExample {
public static void main(String[] args) {
try {
ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ls /nonexistent; echo 'This is a normal message'");
// 将标准错误流合并到标准输出流
pb.redirectErrorStream(true);
Process process = pb.start();
// 只需要一个线程来读取合并后的流
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 所有输出(包括错误)都在这里
}
}
int exitCode = process.waitFor();
System.out.println("\nProcess finished with exit code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果可能会是:
ls: cannot access '/nonexistent': No such or directory
This is a normal message
Process finished with exit code: 0
优点:
- 简单:只需要管理一个输入流,代码更简洁。
缺点:
- 信息丢失:无法区分哪些是标准输出,哪些是标准错误,不利于精确的错误处理和日志分析。
Java 9+ 的 Process API 改进
从 Java 9 开始,Process 类增加了新方法,使得处理输出变得更加方便。
1 ProcessHandle
可以更方便地获取和管理进程信息。
Process process = pb.start();
ProcessHandle processHandle = process.toHandle();
System.out.println("PID: " + processHandle.pid());
2 isAlive()
检查进程是否还在运行。
while (process.isAlive()) {
// 做一些其他事情
}
3 更优雅的输出处理(Java 11+)
Java 11 引入了一些新方法,如 reader(),使得 InputStream 可以直接被当作 BufferedReader 使用,简化了代码。
import java.io.BufferedReader;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
public class Java11ProcessExample {
public static void main(String[] args) {
try {
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "3", "localhost");
Process process = pb.start();
// 使用 CompletableFuture 异步处理输出
CompletableFuture<String> futureOutput = CompletableFuture.supplyAsync(() -> {
// Java 11+ 的 reader() 方法非常方便
try (BufferedReader reader = process.inputReader()) {
return reader.lines().reduce("", (s1, s2) -> s1 + "\n" + s2);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
// 可以同时异步处理错误流...
// ...
System.out.println("Waiting for process to complete...");
int exitCode = process.waitFor();
System.out.println("Process finished with exit code: " + exitCode);
// 获取合并后的输出
String output = futureOutput.get(); // .get() 会等待 future 完成
System.out.println("--- Full Output ---");
System.out.println(output);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种方式结合了现代的异步编程模型,代码更简洁,但需要 Java 11 或更高版本。
将输出重定向到文件
如果你不需要在 Java 程序中实时处理输出,而是希望将其保存到文件,可以使用 ProcessBuilder 的重定向功能,这非常高效,因为它完全由操作系统处理,避免了 Java 程序的 I/O 开销。
import java.io.File;
import java.io.IOException;
public class RedirectToFileExample {
public static void main(String[] args) {
try {
ProcessBuilder pb = new ProcessBuilder("ls", "-l", "/");
// 将标准输出重定向到文件
File outputFile = new File("output.log");
pb.redirectOutput(outputFile);
// 将标准错误也重定向到同一个文件(或另一个文件)
// pb.redirectError(outputFile);
// 或者重定向到标准输出
pb.redirectErrorStream(true);
Process process = pb.start();
int exitCode = process.waitFor();
System.out.println("Command executed. Output saved to " + outputFile.getAbsolutePath());
System.out.println("Process exited with code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
执行后, 目录下的文件列表就会被写入到 output.log 文件中。
总结与最佳实践
| 场景 | 推荐方法 | 优点 | 缺点 |
|---|---|---|---|
| 需要实时处理输出 | ProcessBuilder + Thread |
可靠、灵活、兼容性好 | 代码稍显繁琐 |
不区分 stdout 和 stderr |
ProcessBuilder.redirectErrorStream(true) + 单线程 |
代码简洁 | 无法区分信息来源 |
| Java 11+ 环境 | Process.inputReader() + CompletableFuture |
代码现代、简洁 | 需要 Java 11+ |
| 只需保存到文件,不关心内容 | ProcessBuilder.redirectOutput() |
性能最高,由 OS 处理 | 无法在 Java 中获取输出内容 |
| 诊断死锁问题 | 检查 waitFor() 是否在读取流之前 |
- | - |
核心最佳实践:
- 首选
ProcessBuilder:它比Runtime.exec()更强大、更灵活。 - 永远不要在
waitFor()之前不读取输出流:这是导致死锁最常见的原因。 - 为
stdout和stderr使用独立的消费者线程:这是最健壮、最通用的解决方案。 - 考虑重定向:如果只是需要记录日志,重定向到文件是最高效的选择。
- 利用新 API:如果你的项目使用 Java 9+,请优先使用
ProcessHandle和reader()等新特性来简化代码。
