使用 Runtime.getRuntime().exec() 或 ProcessBuilder 呼叫外部程式 (ffmpeg、swftools、Python 腳本等) 時,若不正確處理子程序的標準輸出/錯誤輸出流,極易導致緩衝區填滿阻塞,造成 Java 主程式掛死。本文深度解析阻塞成因,提供三種層級的解決方案。
Java 進程 子進程 (ffmpeg 等)
┌─────────────┐ ┌─────────────┐
│ Process │ │ stdout/stderr│
│ ├─ getInputStream() ◄──┤ 管道緩衝區 │ (管道大小有限,通常 4KB-64KB)
│ └─ getErrorStream() ◄──┤ 管道緩衝區 │
└─────────────┘ └─────────────┘
│
▼
若 Java 不及時讀取緩衝區 → 緩衝區填滿 → 子進程 write() 阻塞 → 子進程掛起 → Java waitFor() 永不返回
Process process = Runtime.getRuntime().exec(command);
// 或 ProcessBuilder
// Process process = new ProcessBuilder(command).start();
// 必須同步讀取 stdout 和 stderr,否則任一填滿即阻塞
try (BufferedReader stdout = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
BufferedReader stderr = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"))) {
String line;
while ((line = stdout.readLine()) != null) {
System.out.println("[OUT] " + line);
}
while ((line = stderr.readLine()) != null) {
System.err.println("[ERR] " + line);
}
}
int exitCode = process.waitFor(); // 讀完流後再等待結束
System.out.println("進程退出碼: " + exitCode);
缺點:順序讀取 stdout 再 stderr,若 stderr 大量輸出而 stdout 少,會阻塞在第一個 while。
import java.io.*;
import java.util.concurrent.*;
public class ProcessExecutor {
public static int execute(String... command) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(false); // 分離 stdout/stderr
Process process = pb.start();
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
// 非同步讀取 stdout
Future> stdoutFuture = executor.submit(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[OUT] " + line);
}
} catch (IOException e) {
Thread.currentThread().interrupt();
}
});
// 非同步讀取 stderr
Future> stderrFuture = executor.submit(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[ERR] " + line);
}
} catch (IOException e) {
Thread.currentThread().interrupt();
}
});
// 等待進程結束
int exitCode = process.waitFor();
// 等待讀取完成 (設定超時防萬一)
stdoutFuture.get(5, TimeUnit.SECONDS);
stderrFuture.get(5, TimeUnit.SECONDS);
return exitCode;
} finally {
executor.shutdownNow();
}
}
}
import java.io.*;
import java.nio.file.*;
ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", "input.mp4", "output.avi");
// 選項 1: 重定向到檔案
pb.redirectOutput(Paths.get("ffmpeg_out.log").toFile());
pb.redirectError(Paths.get("ffmpeg_err.log").toFile());
// 選項 2: 合併 stderr 到 stdout
pb.redirectErrorStream(true);
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); // 直接繼承到 Java 進程控制台
Process process = pb.start();
int exitCode = process.waitFor();
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.function.Consumer;
public class AsyncProcessExecutor {
public static class Result {
public final int exitCode;
public final String stdout;
public final String stderr;
public final boolean timedOut;
public Result(int exitCode, String stdout, String stderr, boolean timedOut) {
this.exitCode = exitCode;
this.stdout = stdout;
this.stderr = stderr;
this.timedOut = timedOut;
}
}
public static Result execute(long timeoutMs, Consumer stdoutHandler, Consumer stderrHandler, String... command)
throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
Process process = pb.start();
StringBuilder stdoutBuf = new StringBuilder();
StringBuilder stderrBuf = new StringBuilder();
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch streamsDone = new CountDownLatch(2);
// 非同步讀取 stdout
executor.submit(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
stdoutBuf.append(line).append('\n');
if (stdoutHandler != null) stdoutHandler.accept(line);
}
} catch (IOException ignored) {}
finally { streamsDone.countDown(); }
});
// 非同步讀取 stderr
executor.submit(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
stderrBuf.append(line).append('\n');
if (stderrHandler != null) stderrHandler.accept(line);
}
} catch (IOException ignored) {}
finally { streamsDone.countDown(); }
});
// 等待進程結束或超時
boolean finished = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS);
if (!finished) {
process.destroyForcibly(); // JDK 9+
// process.destroy(); // JDK 8
}
// 等待流讀取完成
try { streamsDone.await(2, TimeUnit.SECONDS); }
catch (InterruptedException ignored) {}
executor.shutdownNow();
return new Result(
finished ? process.exitValue() : -1,
stdoutBuf.toString(),
stderrBuf.toString(),
!finished
);
}
}
| 坑點 | 後果 | 解決 |
|---|---|---|
| 只讀 stdout 不讀 stderr | stderr 緩衝區滿 → 子進程阻塞 → Java 掛死 | 必須同時讀取兩個流,或 redirectErrorStream(true) 合併 |
| 順序讀取 (先 stdout 再 stderr) | 若 stderr 大量輸出,阻塞在第一個 while | 多線程並發讀取,或合併流 |
| 不指定字元編碼 | 中文亂碼 (Windows GBK vs Linux UTF-8) | 顯式指定 StandardCharsets.UTF_8 或系統預設 |
| 不設定超時 | 子進程掛起 → Java 永久等待 | waitFor(timeout) + destroyForcibly() |
| 大量輸出導致記憶體暴增 | StringBuilder 無限增長 → OOM | 流式處理、寫檔案、限制緩衝區大小 |
ProcessBuilder 而非 Runtime.exec() (支援目錄、環境變數、重定向)redirectErrorStream(true) + redirectOutput(INHERIT) 或重定向到檔案