最近在用Java调用ffmpeg的命令,所以记录下踩到的坑
如果要在Java中调用shell脚本时,可以使用Runtime.exec或ProcessBuilder.start。它们都会返回一个Process对象,通过这个Process可以对获取脚本执行的输出,然后在Java中进行相应处理。例如,下面的代码
try
{
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
//do something ...
}
catch (Exception e)
{
e.printStackTrace();
}
通常,安全编码规范中都会指出:使用Process.waitfor的时候,可能导致进程阻塞,甚至死锁。 那么这句应该怎么理解呢?用个实际的例子说明下。
问题描述
使用Java代码调用shell脚本,执行后会发现Java进程和Shell进程都会挂起,无法结束。Java代码 processtest.java
try
{
Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start run cmd=" + cmd);
process.waitFor();
System.out.println("finish run cmd=" + cmd);
}
catch (Exception e)
{
e.printStackTrace();
}
被调用的Shell脚本doecho.sh
#!/bin/bash
for((i=0; ;i++))
do
echo -n "0123456789"
echo $i >> count.log
done
挂起原因
主进程中调用Runtime.exec会创建一个子进程,用于执行shell脚本。子进程创建后会和主进程分别独立运行。
因为主进程需要等待脚本执行完成,然后对脚本返回值或输出进行处理,所以这里主进程调用Process.waitfor等待子进程完成。
通过shell脚本可以看出:子进程执行过程就是不断的打印信息。主进程中可以通过Process.getInputStream和Process.getErrorStream获取并处理。
这时候子进程不断向主进程发生数据,而主进程调用Process.waitfor后已挂起。当前子进程和主进程之间的缓冲区塞满后,子进程不能继续写数据,然后也会挂起。
这样子进程等待主进程读取数据,主进程等待子进程结束,两个进程相互等待,最终导致死锁。
解决方法
方案一: 基于上述分析,只要主进程在waitfor之前,能不断处理缓冲区中的数据就可以。因为,我们可以再waitfor之前,单独启两个额外的线程,分别用于处理InputStream和ErrorStream就可以。实例代码如下:
try
{
final Process process = Runtime.getRuntime().exec(cmd);
System.out.println("start run cmd=" + cmd);
//处理InputStream的线程
new Thread()
{
@Override
public void run()
{
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = null;
try
{
while((line = in.readLine()) != null)
{
System.out.println("output: " + line);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
in.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}.start();
new Thread()
{
@Override
public void run()
{
BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line = null;
try
{
while((line = err.readLine()) != null)
{
System.out.println("err: " + line);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
err.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}.start();
process.waitFor();
System.out.println("finish run cmd=" + cmd);
}
catch (Exception e)
{
e.printStackTrace();
}
方案二:那就直接用ProcessBuilder来创建Process对象吧!ProcessBuilder已经给出了这方面的解决方案,但是必须要注意的是ProcessBuilder的redirectErrorStream方法。查API可知晓,redirectErrorStream方法设置为ture的时候,会将getInputStream(),getErrorStream()两个流合并,自动会清空流,无需我们自己处理。如果是false,getInputStream(),getErrorStream()两个流分开,就必须自己处理,程序如下:
public class ProcessBuilderTest {
public static void main(String[] args) {
List params = new ArrayList();
params.add("java");
params.add("-jar");
params.add("ProcessJar.jar");
params.add("args1");
params.add("args2");
params.add("args3");
ProcessBuilder processBuilder = new ProcessBuilder(params);
// System.out.println(processBuilder.directory());
// System.out.println(processBuilder.environment());
processBuilder.redirectErrorStream(true);
try {
Process process = processBuilder.start();
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
System.out.println("exitCode = "+exitCode);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}