每次用Runtime.exec执行一个简单的shell命令,程序就莫名其妙卡在那里不动了?这可能是每个Java开发者都踩过的坑。我刚开始用这个方法时,经常遇到进程挂起的问题,明明命令在终端执行只要几秒钟,在Java里却永远等不到结果。
问题的根源在于I/O流的阻塞。想象一下,你家的水管有三根:一根进水(标准输入),一根出水(标准输出),还有一根排污水(错误输出)。如果出水口被堵住,进水就会停止流动。同样的道理,当Java程序没有及时读取子进程的输出流时,缓冲区满了就会导致整个进程阻塞。
java复制// 典型的问题代码示例
Process process = Runtime.getRuntime().exec("ping www.example.com");
int exitCode = process.waitFor(); // 这里可能会永远阻塞
这段代码的问题在于:ping命令会持续产生输出,但程序没有读取这些输出。当输出缓冲区满时,ping进程就会停止执行,而waitFor()就会一直等待。我在生产环境就遇到过这种情况,一个简单的健康检查命令导致整个服务线程卡死。
Runtime.exec是Java早期的进程创建方式,而ProcessBuilder是Java 5引入的更现代的替代方案。它们底层其实调用的是相同的系统API,但ProcessBuilder提供了更多控制选项。
java复制// Runtime.exec的典型用法
Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "ls -l"});
// ProcessBuilder的典型用法
Process process = new ProcessBuilder("/bin/sh", "-c", "ls -l").start();
看似只是语法糖的区别?实际上ProcessBuilder有几个关键优势:
我在重构一个持续集成系统时,把所有Runtime.exec替换成了ProcessBuilder,不仅解决了流阻塞问题,代码可读性也大幅提升。
理解这两个类的关键是要明白它们的流处理模型。每个子进程都有三个标准流:
这里有个容易混淆的点:从Java程序的角度看,getInputStream()实际上是读取子进程的输出。这种命名是从父进程视角出发的。
最基础的解决方案是为每个流创建一个读取线程:
java复制Process process = new ProcessBuilder("your-command").start();
// 创建线程读取标准输出
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();
}
}).start();
// 创建线程读取错误输出
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();
}
}).start();
int exitCode = process.waitFor();
这种方法虽然有效,但需要为每个命令都写这么多样板代码,维护起来很麻烦。我在早期项目中就这样写过,后来发现代码中到处都是类似的重复逻辑。
ProcessBuilder提供了更优雅的解决方案 - 流重定向:
java复制Process process = new ProcessBuilder("your-command")
.redirectErrorStream(true) // 将错误输出合并到标准输出
.start();
// 现在只需要处理一个流
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("OUTPUT: " + line);
}
}
int exitCode = process.waitFor();
这个方案简洁多了,特别适合不需要区分标准输出和错误输出的场景。我在日志收集系统中就大量使用了这种模式。
对于更复杂的场景,我们可以利用Java 8的Stream API:
java复制public class ProcessStreamer {
private final Process process;
private final ExecutorService executor;
public ProcessStreamer(Process process) {
this.process = process;
this.executor = Executors.newFixedThreadPool(2);
}
public CompletableFuture<Void> streamOutput(Consumer<String> stdoutHandler,
Consumer<String> stderrHandler) {
CompletableFuture<Void> stdoutFuture = CompletableFuture.runAsync(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
stdoutHandler.accept(line);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, executor);
CompletableFuture<Void> stderrFuture = CompletableFuture.runAsync(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
stderrHandler.accept(line);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, executor);
return CompletableFuture.allOf(stdoutFuture, stderrFuture);
}
public void shutdown() {
executor.shutdown();
}
}
这个封装类可以这样使用:
java复制Process process = new ProcessBuilder("your-command").start();
ProcessStreamer streamer = new ProcessStreamer(process);
streamer.streamOutput(
line -> System.out.println("OUT: " + line),
line -> System.err.println("ERR: " + line)
).thenRun(() -> {
System.out.println("Process completed with exit code: " + process.exitValue());
streamer.shutdown();
});
这种方案适合需要精细控制输出处理的场景,比如需要将不同级别的日志输出到不同目的地的情况。
在高并发场景下,进程管理不当会导致严重问题。我曾经遇到过一个案例:一个定时任务系统没有正确销毁进程,导致系统中积累了上千个僵尸进程,最终使服务器崩溃。
关键问题包括:
下面是一个经过实战检验的进程管理方案:
java复制public class ProcessExecutor {
private final ProcessBuilder processBuilder;
private final long timeout;
private final TimeUnit timeUnit;
public ProcessExecutor(ProcessBuilder processBuilder, long timeout, TimeUnit timeUnit) {
this.processBuilder = processBuilder;
this.timeout = timeout;
this.timeUnit = timeUnit;
}
public ExecutionResult execute() throws IOException, InterruptedException, TimeoutException {
Process process = processBuilder.start();
try {
// 使用CompletableFuture处理超时
CompletableFuture<Integer> exitCodeFuture = process.onExit().thenApply(Process::exitValue);
// 处理输出流
StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");
outputGobbler.start();
errorGobbler.start();
try {
int exitCode = exitCodeFuture.get(timeout, timeUnit);
return new ExecutionResult(exitCode, outputGobbler.getOutput(), errorGobbler.getOutput());
} catch (TimeoutException e) {
// 超时后强制销毁进程树
destroyProcessTree(process);
throw e;
} catch (ExecutionException e) {
throw new IOException("Process execution failed", e.getCause());
}
} finally {
// 确保流被关闭
closeQuietly(process.getInputStream());
closeQuietly(process.getOutputStream());
closeQuietly(process.getErrorStream());
}
}
private void destroyProcessTree(Process process) {
// 获取进程ID
long pid = getProcessId(process);
// 不同操作系统使用不同命令
String[] command = OS.isWindows()
? new String[]{"taskkill", "/F", "/T", "/PID", String.valueOf(pid)}
: new String[]{"pkill", "-P", String.valueOf(pid)};
try {
new ProcessBuilder(command).start().waitFor();
} catch (IOException | InterruptedException e) {
process.destroyForcibly();
}
}
private static class StreamGobbler extends Thread {
private final InputStream inputStream;
private final String type;
private final StringBuilder output = new StringBuilder();
StreamGobbler(InputStream inputStream, String type) {
this.inputStream = inputStream;
this.type = type;
setDaemon(true);
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
System.out.println(type + "> " + line);
}
} catch (IOException e) {
System.err.println("Error reading " + type + " stream: " + e.getMessage());
}
}
String getOutput() {
return output.toString();
}
}
public static class ExecutionResult {
private final int exitCode;
private final String output;
private final String error;
// 构造函数和getter省略
}
}
这个方案有几个关键优势:
在高并发环境下,还需要考虑以下优化点:
java复制// 进程池示例
public class ProcessPool {
private final BlockingQueue<Process> pool;
private final ProcessBuilder templateBuilder;
public ProcessPool(int size, ProcessBuilder templateBuilder) throws IOException {
this.pool = new ArrayBlockingQueue<>(size);
this.templateBuilder = templateBuilder;
// 预热池
for (int i = 0; i < size; i++) {
pool.add(templateBuilder.start());
}
}
public Process borrowProcess() throws InterruptedException {
return pool.take();
}
public void returnProcess(Process process) {
if (!pool.offer(process)) {
process.destroy();
}
}
public void close() {
pool.forEach(Process::destroy);
pool.clear();
}
}
这种池化技术特别适合需要频繁执行相同命令的场景,比如批量处理脚本。