解決Process.getInputStream()阻塞的問題

Process.getInputStream()阻塞問題

Java中

Runtime.getInstance().exec (String cmd)

或者

new ProcessBuilder(String cmd).start()

都可以產生子進程對象Process。通過調用Process對象的waitFor()方法可以使主進程進入等待狀態,直至子進程執行完畢,再進行下一步工作。如果對子進程處理不當,有可能造成主進程阻塞,整個程序死掉。

java Api中關於Process說的是:

ProcessBuilder.start() 和 Runtime.exec 方法創建一個本機進程,並返回 Process 子類的一個實例,該實例可用來控制進程並獲取相關信息。Process 類提供瞭執行從進程輸入、執行輸出到進程、等待進程完成、檢查進程的退出狀態以及銷毀(殺掉)進程的方法。

創建進程的方法可能無法針對某些本機平臺上的特定進程很好地工作,比如,本機窗口進程,守護進程,Microsoft Windows 上的 Win16/DOS 進程,或者 shell 腳本。創建的子進程沒有自己的終端或控制臺。它的所有標準 io(即 stdin,stdout,stderr)操作都將通過三個流 (getOutputStream(),getInputStream(),getErrorStream()) 重定向到父進程。父進程使用這些流來提供到子進程的輸入和獲得從子進程的輸出。因為有些本機平臺僅針對標準輸入和輸出流提供有限的緩沖區大小,如果讀寫子進程的輸出流或輸入流迅速出現失敗,則可能導致子進程阻塞,甚至產生死鎖。

在對getOutputStream(),getInputStream(),getErrorStream()的描述中,有個註意事項:對其輸出流和錯誤流進行緩沖是一個好主意!嗯,好抽象啊!

問題正在於此,Process.getInputStream()和Process.getErrorStream()分別返回Process的標準輸出流和錯誤流,兩個流如果處理不當,其緩沖區不能被及時清除而被塞滿,則進程被阻塞,即使調用Process.destory()也未必能銷毀被阻塞的子進程。

如果嘗試同步獲取Process的輸出流和錯誤流進行處理,未必有效,順序執行過程中,輸出流和錯誤流常常不能得到及時處理。解決方案有兩個。

方案一:並發獲取Process的輸出流和錯誤流

通過啟動兩個線程來並發地讀取和處理輸出流和錯誤流,懶得打開IDE瞭,就大概敲一下代碼吧,可能有錯誤,如下:

調用者:

class ProcessExecutor
{
 private Process p;
 private List<String> outputList;
 private List<String> errorOutputList;
 public ProcessExecutor(Process p) throws IOException
 {
  if(null == p)
  {
   throw new IOException("the provided Process is null");
  }
  this. p = p;
 }
 public List<String> getOutputList()
 {
  return this. outputList;
 }
 public List<String> getErrorOutputList()
 {
  return this.errorOutputList;
 }
 public int execute()
 {
  int rs = 0;
  Thread outputThread = new ProcessOutputThread(this.p.getInputStream());
  Thread errorOutputThread = new ProcessOutputThread(this.p.getErrorStream());
  outputThread.start();
  errorOutputThread.start();
  rs = p.waitFor();
  outputThread.join();
  errorOutputThread.join();
  this.outputList = outputThread.getOutputList();
  this.errorOutputList = errorOutputThread.getOutputList();
  return rs;
 }
}

流處理線程

class ProcessOutputThread extends Thread
{
 private InputStream is;
 private List<String> outputList;
 public ProcessOutputThread(InputStream is) throws IOException
 {
  if(null == is)
  {
   throw new IOException("the provided InputStream is null");
  }
  this. is = is;
  this.outputList = new ArrayList<String>();
 }
 public List<String> getOutputList()
 {
  return this. outputList;
 }
 @Override
 public void run()
 {
  InputStreamReader ir = null;
  BufferedReader br = null;
  try
  {
   ir = new InputStreamReader(this.is);
   br = new BufferedReader(ir);
   String output = null;
   while(null != (output = br.readLine()))
   {
    print(output);
    this.outputList.add(output);
   }
  }
  catch(IOException e)
  {
   e.print();
  }
  finally
  (
   try
   {
    if(null != br)
    {
     br.close();
    }
    if(null != ir)
    {
     ir.close();
    }
    if(null != this.is)
    {
     this.is.close();
    }
   }
   catch(IOException e)
   {
    e.print();
   }
  )
 }
}

方案二:用ProcessBuilder的redirectErrorStream()方法合並輸出流和錯誤流

public int execute()
{
 int rs = 0;
 String[] cmds = {...};//command and arg  
 ProcessBuilder builder = new ProcessBuilder(cmds);  
 builder.redirectErrorStream(true);  
 Process process = builder.start();  
 BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));  
 String output = null;  
 while (null != (readLine = br.readLine()))
 {  
     print(output);   
 }  
 rs = process.waitFor();
 return rs;
} 

Java Process 阻塞測試總結

Process阻塞原因:輸入流和錯誤流分開的,沒有處理,就會發生阻塞,歸根結底本質上是bio引起的io阻塞問題。

getInputStream,getErrorSteam就是獲取腳本或者命令的控制臺回顯信息,前者獲取的是標準輸出的回顯信息,後者獲取的是標準錯誤的回顯信息

Process原理:使用Runtime.getRuntime().exec(cmd)會在當前進程建立一個子進程,子進程由於沒有控制臺,它的標準輸出和標準錯誤就會返回給父進程Process,因此通過getInputStream和getErrorStream就可以獲取到這些信息。

測試代碼如下:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class JavaExeBat {
        public JavaExeBat() {
        }
        public static void main(String[] args) {
                Process p;
                //test.bat中的命令是ipconfig/all
                String cmd="sh test.sh ";
                //String cmd="ping 127.0.0.1 -c 4";
 
                try {
                        //執行命令
                        p = Runtime.getRuntime().exec(cmd);
                        //取得命令結果的輸出流
                        //輸出流
                        InputStream fis=p.getInputStream();
                        //錯誤流
                        InputStream ferrs=p.getErrorStream();
                        //用一個讀輸出流類去讀
                        InputStreamReader isr=new InputStreamReader(fis);
                        InputStreamReader errsr=new InputStreamReader(ferrs);
                        //用緩沖器讀行
                        BufferedReader br=new BufferedReader(isr);
                        BufferedReader errbr=new BufferedReader(errsr);
                        String line=null;
                        String lineerr = null;
                        //直到讀完為止
                        while((line=br.readLine())!=null) {
                        //有可能發生阻塞的問題
                                System.out.println("return input Str:" + line);
                        }
                        while((lineerr=errbr.readLine())!=null){
                        //有可能發生阻塞的問題
                                System.out.println("return err Str:" + lineerr);
                        }
                        int exitVal = p.waitFor();
                        System.out.println("exitVal:" + exitVal);
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }
}

test.sh如下

#!/bin/bash
 
for((i=0; i < 100000; i++));do
         //輸出的標準輸出
        echo "testaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
        //輸出到標準錯誤
        echo "testaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1>&2
done

經過測試發現,如果JavaExeBat.java文件中隻開啟標準輸出或者標準錯誤時,進程就會夯住,無法通過waiteFor獲取其返回值,因為腳本中分別輸出瞭100000w條信息到標準輸出和標準錯誤,而下述代碼隻處理瞭getInputStream,導致標準錯誤輸出流的信息太多返回給當前進程,沒有得到處理,因此阻塞。

代碼如下:

p = Runtime.getRuntime().exec(cmd);
                        //取得命令結果的輸出流
                        //輸出流
                        InputStream fis=p.getInputStream();
                        //用一個讀輸出流類去讀
                        InputStreamReader isr=new InputStreamReader(fis);
                        //用緩沖器讀行
                        BufferedReader br=new BufferedReader(isr);
                        String line=null;
                        //直到讀完為止
                        while((line=br.readLine())!=null) {
                        //有可能發生阻塞的問題
                                System.out.println("return input Str:" + line);
                        }
                        int exitVal = p.waitFor();
                        System.out.println("exitVal:" + exitVal);

把上述代碼中的getInputStream換做getErrorStream,也會夯住進程,因為同樣隻處理瞭兩者中一者,即標準錯誤。

那麼能不能同步處理兩個流信息呢?代碼如下:

try {
                        //執行命令
                        p = Runtime.getRuntime().exec(cmd);
                        //取得命令結果的輸出流
                        //輸出流
                        InputStream fis=p.getInputStream();
                        //錯誤流
                        InputStream ferrs=p.getErrorStream();
                        //用一個讀輸出流類去讀
                        InputStreamReader isr=new InputStreamReader(fis);
                        InputStreamReader errsr=new InputStreamReader(ferrs);
                        //用緩沖器讀行
                        BufferedReader br=new BufferedReader(isr);
                        BufferedReader errbr=new BufferedReader(errsr);
                        String line=null;
                        String lineerr = null;
                        //直到讀完為止
                        while((line=br.readLine())!=null) {
                        //有可能發生阻塞的問題
                                System.out.println("return input Str:" + line);
                        }
                        while((lineerr=errbr.readLine())!=null){
                        //有可能發生阻塞的問題
                                System.out.println("return err Str:" + lineerr);
                        }
                        int exitVal = p.waitFor();
                        System.out.println("exitVal:" + exitVal);
                } catch (Exception e) {
                        e.printStackTrace();
                }
        }

測試過後發現也不行,因為是同步的,就會有先後順序,也會發生阻塞,測試方法,將test.sh改為隻打印標準錯誤,就會發現標準錯誤處理被阻塞,腳本如下:

#!/bin/bash
 
for((i=0; i < 100000; i++));do
        //輸出到標準錯誤
        echo "testaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" 1>&2
done

解決辦法思路:

(1)並發處理兩個流信息,開啟兩個線程分別處理輸出流與錯誤流

(2)將兩個流合並為一個流解決示例:

第一種思路:

class ProcessExecutor  
    {  
        private Process p;  
        private List<String> outputList;  
        private List<String> errorOutputList;  
        public ProcessExecutor(Process p) throws IOException  
        {  
            if(null == p)  
            {  
                throw new IOException("the provided Process is null");  
            }  
            this. p = p;  
        }  
        public List<String> getOutputList()  
        {  
            return this. outputList;  
        }  
        public List<String> getErrorOutputList()  
        {  
            return this.errorOutputList;  
        }  
        public int execute()  
        {  
            int rs = 0;  
            Thread outputThread = new ProcessOutputThread(this.p.getInputStream());  
            Thread errorOutputThread = new ProcessOutputThread(this.p.getErrorStream());  
            outputThread.start();  
            errorOutputThread.start();  
            rs = p.waitFor();  
            outputThread.join();  
            errorOutputThread.join();  
            this.outputList = outputThread.getOutputList();  
            this.errorOutputList = errorOutputThread.getOutputList();  
            return rs;  
        }  
    }  
    
    class ProcessOutputThread extends Thread  
    {  
        private InputStream is;  
        private List<String> outputList;  
        public ProcessOutputThread(InputStream is) throws IOException  
        {  
            if(null == is)  
            {  
                throw new IOException("the provided InputStream is null");  
            }  
            this. is = is;  
            this.outputList = new ArrayList<String>();  
        }  
        public List<String> getOutputList()  
        {  
            return this. outputList;  
        }  
        @Override  
        public void run()  
        {  
            InputStreamReader ir = null;  
            BufferedReader br = null;  
            try  
            {  
                ir = new InputStreamReader(this.is);  
                br = new BufferedReader(ir);  
                String output = null;  
                while(null != (output = br.readLine()))  
                {  
                    print(output);  
                    this.outputList.add(output);  
                }  
            }  
            catch(IOException e)  
            {  
                e.print();  
            }  
            finally  
            (  
                try  
                {  
                    if(null != br)  
                    {  
                        br.close();  
                    }  
                    if(null != ir)  
                    {  
                        ir.close();  
                    }  
                    if(null != this.is)  
                    {  
                        this.is.close();  
                    }  
                }  
                catch(IOException e)  
                {  
                    e.print();  
                }  
            )  
        }  
    }  

第二種思路:使用ProcessBuilder,將其redirectErrorStream(true);將輸出流與錯誤流合並

 public int execute()  
    {  
        int rs = 0;  
        String[] cmds = {...};//command and arg    
        ProcessBuilder builder = new ProcessBuilder(cmds);    
        builder.redirectErrorStream(true);    
        Process process = builder.start();    
        BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));    
        String output = null;    
        while (null != (readLine = br.readLine()))  
        {    
            print(output);     
        }    
        rs = process.waitFor();  
        return rs;  
    }  

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

推薦閱讀: