首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Runtime.exec 阻塞問題完整解決方案

     編輯:Java 相關知識

Java 執行外部程式阻塞死鎖徹底解析與解決

使用 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();
        }
    }
}

方案三:ProcessBuilder redirectOutput (JDK 7+ 最簡潔)


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 不讀 stderrstderr 緩衝區滿 → 子進程阻塞 → Java 掛死必須同時讀取兩個流,或 redirectErrorStream(true) 合併
順序讀取 (先 stdout 再 stderr)若 stderr 大量輸出,阻塞在第一個 while多線程並發讀取,或合併流
不指定字元編碼中文亂碼 (Windows GBK vs Linux UTF-8)顯式指定 StandardCharsets.UTF_8 或系統預設
不設定超時子進程掛起 → Java 永久等待waitFor(timeout) + destroyForcibly()
大量輸出導致記憶體暴增StringBuilder 無限增長 → OOM流式處理、寫檔案、限制緩衝區大小

最佳實踐總結

  1. 優先使用 ProcessBuilder 而非 Runtime.exec() (支援目錄、環境變數、重定向)
  2. 簡單場景:redirectErrorStream(true) + redirectOutput(INHERIT) 或重定向到檔案
  3. 需捕獲輸出:多線程讀取 stdout/stderr,或使用成熟庫 (Apache Commons Exec、ZT Process Executor)
  4. 必須設定超時機制,防止惡意/異常子進程拖垮主程式
  5. 生產環境考慮容器化隔離、資源限制 (cgroups)、安全策略 (seccomp)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved