使用Runtime 調用Process.waitfor導致的阻塞問題

1. 關於Runtime類的小知識

  • Runtime.getRuntime()可以取得當前JVM的運行時環境,這也是在Java中唯一一個得到運行時環境的方法
  • Runtime中的exit方法是退出JVM

2. Runtime的幾個重要的重載方法

方法名 作用
exec(String command); 在單獨的進程中執行指定的字符串命令。
exec(String command, String[] envp) 在指定環境的單獨進程中執行指定的字符串命令。
exec(String[] cmdarray, String[] envp, File dir) 在指定環境和工作目錄的獨立進程中執行指定的命令和變量
exec(String command, String[] envp, File dir) 在有指定環境和工作目錄的獨立進程中執行指定的字符串命令。

Runtime類的重要的方法還有很多,簡單列舉幾個

  • exit(int status):終止當前正在運行的 Java 虛擬機
  • freeMemory():返回 Java 虛擬機中的空閑內存量。
  • load(String filename): 加載作為動態庫的指定文件名。
  • loadLibrary(String libname): 加載具有指定庫名的動態庫。

3. Runtime的使用方式

錯誤的使用exitValue()

  public static void main(String[] args) throws IOException {
        String command = "ping www.baidu.com";
        Process process = Runtime.getRuntime().exec(command);
        int i = process.exitValue();
        System.out.println("字進程退出值:"+i);
    }

輸出:

Exception in thread “main” java.lang.IllegalThreadStateException: process has not exited
at java.lang.ProcessImpl.exitValue(ProcessImpl.java:443)
at com.lirong.think.runtime.ProcessUtils.main(ProcessUtils.java:26)

原因:

exitValue()方法是非阻塞的,在調用這個方法時cmd命令並沒有返回所以引起異常。阻塞形式的方法是waitFor,它會一直等待外部命令執行完畢,然後返回執行的結果。

修改後的版本:

  public static void main(String[] args) throws IOException {
        String command = "javac";
        Process process = Runtime.getRuntime().exec(command);
        process.waitFor();
        process.destroy();
        int i = process.exitValue();
        System.out.println("字進程退出值:"+i);
    }

此版本已然可以正常運行,但當主線程和子線程有很多交互的時候還是會出問題,會出現卡死的情況。

4. 卡死原因

  • 主進程中調用Runtime.exec會創建一個子進程,用於執行cmd命令。子進程創建後會和主進程分別獨立運行。
  • 因為主進程需要等待腳本執行完成,然後對命令返回值或輸出進行處理,所以這裡主進程調用Process.waitfor等待子進程完成。
  • 運行此cmd命令可以知道:子進程執行過程就是打印信息。主進程中可以通過Process.getInputStream和Process.getErrorStream獲取並處理。
  • 這時候子進程不斷向主進程發生數據,而主進程調用Process.waitfor後已掛起。當前子進程和主進程之間的緩沖區塞滿後,子進程不能繼續寫數據,然後也會掛起。
  • 這樣子進程等待主進程讀取數據,主進程等待子進程結束,兩個進程相互等待,最終導致死鎖。

5. 解決方案

不斷的讀取消耗緩沖區的數據,以至子進程不會掛起,下面是具體代碼:

/**
 * @author lirong
 * @desc CMD命令測試
 * @date 2019/06/13 20:50
 */
@Slf4j
public class ProcessUtils {
    public static void main(String[] args) throws IOException, InterruptedException {
        String command = "ping www.baidu.com";
        Process process = Runtime.getRuntime().exec(command);
        readStreamInfo(process.getInputStream(), process.getErrorStream());
        int exit = process.waitFor();
        process.destroy();
        if (exit == 0) {
            log.debug("子進程正常完成");
        } else {
            log.debug("子進程異常結束");
        }
    }
    /**
     * 讀取RunTime.exec運行子進程的輸入流 和 異常流
     * @param inputStreams 輸入流
     */
    public static void readStreamInfo(InputStream... inputStreams){
        ExecutorService executorService = Executors.newFixedThreadPool(inputStreams.length);
        for (InputStream in : inputStreams) {
            executorService.execute(new MyThread (in));
        }
        executorService.shutdown();
    }
}
/**
 * @author lirong
 * @desc
 * @date 2019/06/13 21:25
 */
@Slf4j
public class MyThread implements Runnable {
    private InputStream in;
    public MyThread(InputStream in){
        this.in = in;
    }
    @Override
    public void run() {
        try{
            BufferedReader br = new BufferedReader(new InputStreamReader(in, "GBK"));
            String line = null;
            while((line = br.readLine())!=null){
                log.debug(" inputStream: " + line);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

寫到這裡大傢以為都結束瞭哇,並沒有,哈哈哈,真實的生成環境總能給你帶來很多神奇的問題,Runtime不僅可以直接調用CMD執行命令,還可以調用其他.exe程序執行命令。

所以縱使你讀取瞭緩沖區的數據,你的程序依然可能會被卡死,因為有可能你的緩沖區根本就沒有數據,而是你的.exe程序卡主瞭。嗯,所以為你以防萬一,你還需要設置超時。

6. Runtime最優雅的調用方式

/**
 * @author lirong
 * @desc
 * @date 2019/06/13 20:50
 */
@Slf4j
public class ProcessUtils {
   /**
     * @param timeout 超時時長
     * @param fileDir 所運行程序路徑
     * @param command 程序所要執行的命令
     * 運行一個外部命令,返回狀態.若超過指定的超時時間,拋出TimeoutException
     */
    public static int executeProcess(final long timeout, File fileDir, final String[] command)
            throws IOException, InterruptedException, TimeoutException {
        Process process = Runtime.getRuntime().exec(command, null, fileDir);
        Worker worker = new Worker(process);
        worker.start();
        try {
            worker.join(timeout);
            if (worker.exit != null){
                return worker.exit;
            } else{
                throw new TimeoutException();
            }
        } catch (InterruptedException ex) {
            worker.interrupt();
            Thread.currentThread().interrupt();
            throw ex;
        }
        finally {
            process.destroy();
        }
    }
    
    private static class Worker extends Thread {
        private final Process process;
        private Integer exit;
        private Worker(Process process) {
            this.process = process;
        }
        @Override
        public void run() {
            InputStream errorStream = null;
            InputStream inputStream = null;
            try {
                errorStream = process.getErrorStream();
                inputStream = process.getInputStream();
                readStreamInfo(errorStream, inputStream);
                exit = process.waitFor();
                process.destroy();
                if (exit == 0) {
                    log.debug("子進程正常完成");
                } else {
                    log.debug("子進程異常結束");
                }
            } catch (InterruptedException ignore) {
                return;
            }
        }
    }
    /**
     * 讀取RunTime.exec運行子進程的輸入流 和 異常流
     * @param inputStreams 輸入流
     */
    public static void readStreamInfo(InputStream... inputStreams){
        ExecutorService executorService = Executors.newFixedThreadPool(inputStreams.length);
        for (InputStream in : inputStreams) {
            executorService.execute(new MyThread(in));
        }
        executorService.shutdown();
    }
}

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: