您的位置 首页 java

JAVA中外部进程输出流处理误区

java 中通过 Runtime 启动一个外部进程是一个常见的做法,但是如果外部进程的输出流没有被正确的处理,往往会带来一些意想不到的结果。最近我就遇到过一次这样的问题。

在我的程序中,有这么一个功能,启动一个外部进程,获取该外部进程的输入流和输出流。这里权且将我的程序称为主程序,在主程序中启动起来的程序称做 子程序 。在主程序中,我们会拿到子程序的标准输入流和标准输出流,基于这两个流,主程序和子程序进行通信(不用问我为什么不用 socket ,这里有一堆理由~)。为了避免子程序空闲太长时间,子程序在空闲一段时间后,调用 System.exit(-1) 结束进程。

后来我们遇到什么问题呢?我们发现在某些环境上,程序跑一段时间后, CPU 会发生spike。经过一些定位后,我们发现CPU的SPIKE总是发生在子程序调用 System.exit(-1) 后,且子程序并没有退出,反而开始占用大量的CPU,单个进程大概占20%左右(想象我们会启动多个子进程,结果就是系统的CPU程序阶梯状的跳跃)。这时候我们顺理成章地上了jstack进行堆栈dump,通过对堆栈的分析,我们发现程序hang住在 java.util.logging.LogManager 的一个 shutdown hooker上,该hooker其实只做了一件事情,就是刷新标准错误流的buffer:

 System.err.flush();  

到这里,很明显,是因为子程序的标准错误流被遗忘了,我们虽然消耗了其标准输出流,却没有处理其错误流。所以,赶紧fix,fix的方法就是在主程序中添加一个错误流消耗线程。在关闭子程序的时候,我们会先关闭对应的错误流。

  static  class ErrStreamTask  extends  Thread {
   private  final  InputStream  is;

  ErrStreamTask(InputStream is) {
    this.is = is;
  }

  @Override
  public  void  run() {
    try {
      int len = 0;  byte [] buffer = new byte[1024];
      while ((len = is.read(buffer)) != 0) { // may block is.close() in windows.
        // handle out
      }
    }
    catch (Throwable e) {
      // log
    }
  }

  public void close() {
    try {
      is.close();
    }
    catch (IO Exception  e) {
      e.printStackTrace();
    }
  }
}  

这个问题的解决有两种方法:

  1. 在我们这个场景中,可以不用显示的去关闭输出流。我们在掉process.destory()时error stream也会被关闭
  2. 采用类似 EPOLL 轮训策略,在调用阻塞的 read() 方法钱,调用非阻塞的 available() 方法,该方法会得到流总可读的字节数(这个结果不精确),确保流中有数据了再调用阻塞的 read() 方法。

我们选取的是方案2。

经过这么一次折腾,还是有这么一些启发和思考:

  1. 对于程序内启动的外部程序,一定要记得消耗外部进程的标准输出流,切记还有标准错误流(有些三方库会利用标准错误流打日志啊!!!!)
  2. 开发过程中,关键路径的日志很重要,遇到问题时,日志是定位问题的一大利器,对快速分析和定位有很大的帮助
  3. 最后嘛,windows系统是一个让 程序员 又爱又恨的系统。好在现在大部分的服务都是泡在 linux 平台上。

文章来源:智云一二三科技

文章标题:JAVA中外部进程输出流处理误区

文章地址:https://www.zhihuclub.com/169692.shtml

关于作者: 智云科技

热门文章

网站地图