前些天使用Java调用外部程序的时候,发现线程会堵塞在waitfor()方法。
调用方法如下:
如果直接在Shell中调用这个程序,程序会很快结束,不会僵死。
为什么会堵塞呢,原因是当调用exec(cmd)后,JVM会启动一个子进程,该进程会与JVM进程建立3个管道连接,标准输入,标准输出和标准错误流。假设该程序不断在向标准输出流和标准错误流写数据,而JVM不读取,数据会暂时缓冲在Linux的缓冲区,缓冲区满后该程序将无法继续写数据,会僵死,所以Java程序就会僵死在waitfor(),永远无法结束。
解决办法就是增加两个线程,一个线程负责读标准输出流,另一个负责读标准错误流,这样子数据就不会积压在缓冲区,程序就能够顺利运行。
查看源代码后,还发现一个潜在的问题。但程序执行到exec的时候,JVM会使用管道,占有3个文件句柄,但程序运行结束后,这三个句柄并不会自动关闭,这样最终会导致java.io.IOException: Too many open files。所以就算外部程序的没有输出,也必须关闭句柄:
我们发觉当调用close()方法后,JVM并不会立即回收句柄,具体的回收时间不确定。另外如果不调用close(),句柄也会被回收,也可能发生“Too many open files”的错误。根据这篇文章,不同的垃圾收集器会选择不同的回收策略。所以最好还是要关闭。
总结
1.如果外部程序有大量输出,需要启动额外的线程来读取标准输出和标准错误流
2.必须关闭三个句柄
import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class ProcessUtil { private static final int DEFAULT_BUFFER_SIZE = 1024; private static Log log = LogFactory.getLog(ProcessUtil.class); private InputStream in; private OutputStream out; private OutputStream err; public ProcessUtil(OutputStream out, OutputStream err, InputStream in) { if (out == null) { out = new NullOutputStream(); } if (err == null) { err = new NullOutputStream(); } this.out = out; this.err = err; this.in = in; } public int process(String cmd, String[] envp, File dir) throws IOException, InterruptedException { Process p = Runtime.getRuntime().exec(cmd, envp, dir); return process(p); } public int process(String[] cmdarray, String[] envp, File dir) throws IOException, InterruptedException { Process p = Runtime.getRuntime().exec(cmdarray, envp, dir); return process(p); } private int process(Process p) throws IOException, InterruptedException { try { OutputStream pin = p.getOutputStream(); StreamGobbler outg = new StreamGobbler(p.getInputStream(), out); StreamGobbler errg = new StreamGobbler(p.getErrorStream(), err); outg.start(); errg.start(); if (in != null) { byte[] inBuf = new byte[DEFAULT_BUFFER_SIZE]; int inN = 0; while (-1 != (inN = in.read(inBuf))) { pin.write(inBuf, 0, inN); } pin.flush(); } int code = p.waitFor(); outg.join(); errg.join(); return code; } finally { closeQuietly(p.getOutputStream()); closeQuietly(p.getInputStream()); closeQuietly(p.getErrorStream()); } } private void closeQuietly(Closeable closeable) { try { if (closeable != null) closeable.close(); } catch (Exception e) { log.warn("close error", e); } } } class StreamGobbler implements Runnable { private static final int DEFAULT_BUFFER_SIZE = 1024; private static Log log = LogFactory.getLog(StreamGobbler.class); private InputStream is; private OutputStream os; private Thread thread; public StreamGobbler(InputStream is, OutputStream os) { this.is = is; this.os = os; } public void start() { thread = new Thread(this); thread.setDaemon(true); thread.start(); } public void run() { try { byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; int n = 0; while (-1 != (n = is.read(buf))) { os.write(buf, 0, n); } os.flush(); } catch (Exception ex) { log.warn("stream error", ex); } } public void join() throws InterruptedException { thread.join(); } } class NullOutputStream extends OutputStream { public static final NullOutputStream NULL_OUTPUT_STREAM = new NullOutputStream(); @Override public void write(byte[] b, int off, int len) { } @Override public void write(int b) { } @Override public void write(byte[] b) throws IOException { } }