原文链接: http://freewind886.blog.163.com/blog/static/66192464201261462759238/
由于使用ProcessBuilder 发生了阻塞 ,根据方法4搞定,记录下!
前段时间实现一个小功能,在长时间运行的管理服务器master(Java进程)上增加一种调用shell脚本发送报警的方式(已有邮件和短信报警)。脚本名称和相对路径固定,每发送一次报警master就会调用一次脚本(可能会很频繁),报警内容是JSON格式的消息,以$1参数传入脚本。用户可以自定义shell脚本的内容,例如再调用python脚本将报警内容发送到指定的服务器。master只负责调用脚本传入报警信息,确保执行脚本的线程能合理退出,如果有异常也要打印错误日志方便问题排查。
方案1
class ExecuteThread extends Thread { private String cmdString;
ExecuteThread(String cmd) { this.cmdString = cmd }
public void run() { String[] cmdArray = { "/bin/bash", "-c", cmdString }; ProcessBuilder builder = new ProcessBuilder(cmdArray); process = builder.start();
// 获取错误输出 InputStream stderr = process.getErrorStream();
// 使用Reader进行输入读取和打印 InputStreamReader isr = new InputStreamReader(stderr); BufferedReader br = new BufferedReader(isr); String line = null; System.out.println("
// 获取执行返回值 int exitCode = process.waitFor(); if (exitCode != 0) { // 进行错误处理 } } }
方案1的问题在于当脚本本身有问题导致执行时间过长时,整个操作就会在读取输出的地方卡住(br.readLine()),整个执行外部脚本操作的线程就会卡住无法退出。
方案2
对方案1进行改进,将循环读取输出的操作挪到另一个线程中进行。
这篇文章对这种方式以及执行外部脚本相关的细节有很详细的说明,推荐细看:http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html
class ExecuteThread extends Thread { private String cmdString;
ExecuteThread(String cmd) { this.cmdString = cmd }
public void run() { String[] cmdArray = { "/bin/bash", "-c", cmdString }; ProcessBuilder builder = new ProcessBuilder(cmdArray); process = builder.start();
// 获取错误输出 InputStream stderr = process.getErrorStream();
// 使用StreamGobbler进行输入读取和打印 new StreamGobbler(stderr).start();
// 获取执行返回值 int exitCode = process.waitFor(); if (exitCode != 0) { // 进行错误处理 } } }
class StreamGobbler extends Thread { private InputStream input;
public StreamGobbler(InputStream input) { this.input = input; }
public void run() { InputStreamReader isr = new InputStreamReader(input); BufferedReader br = new BufferedReader(isr); String line = null; System.out.println("
对于脚本执行超时的问题,方案2没有解决,执行输出读取的StreamGobbler线程还是会因为readLine()卡住,而ExecuteThread会由于process.waitFor()而卡住。 如果每调用一次脚本就多出2个无法退出的线程,那master迟早会因为资源耗尽而崩溃。
方案3
对于Process.waitFor()的阻塞,可以调用Process.destroy()解除,而对于readLine()的阻塞,则尝试使用Reader.close或InputStream.close()来解除。
class ExecuteThread extends Thread { // 增加close()方法,外部判定任务执行超时后进行资源清理 // 其他部分代码不变 public void close() { process.destroy(); stderr.close(); } }
// 使用ExecuteThread的静态方法 static void executeCmd(String cmd) { ExecuteThread execThread = new ExecuteThread(cmd).start(); try { execThread.join(timeoutInMillis); } finally { execThread.close(); } }
针对方案3进行了多次的测试,Process.destroy()可以解除Process.waitFor()的阻塞,而stderr.close()却无法让阻塞的readLine()中断退出。也就是说,当用户写了个有问题的脚本,每次都执行很长一段时间甚至不退出,那么每发送一次报警就多1个阻塞的StreamGobbler线程,方案2中存在的问题没有解决。
方案4
在这个线程线程退出的问题上,也尝试了不少的方案,最后找到一个比较丑的办法,在sun jdk1.5和1.6上测试通过。
class ExecuteThread extends Thread { private String cmdString; private volatile Process process; private volatile FileChannel inputChannel;
ExecuteThread(String cmd) { this.cmdString = cmd }
public void run() { String[] cmdArray = { "/bin/bash", "-c", cmdString }; ProcessBuilder builder = new ProcessBuilder(cmdArray); process = builder.start();
// 获取脚本错误输出 InputStream errorStream = process.getErrorStream(); if (errorStream instanceof FileInputStream) { inputChannel = ((FileInputStream) errorStream).getChannel(); } else { throw new Exception("无法将脚本子进程的输出流转为管道"); }
// inputChannel.read(buffer)会因为inputChannel的关闭而退出,不会一直阻塞; /* 以下的处理也可以用另一个线程来执行,这里放在同一个线程是使得在调用脚本出错(例如脚本文件被误删除) 迅速退出的情况下也能获取到相应的错误信息,避免ExecuteThread比读取输出线程结束得更快。
*/ ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(32); ByteBuffer buffer = ByteBuffer.allocate(32); WritableByteChannel channelOut = Channels.newChannel(byteArrayStream); try { while (inputChannel.read(buffer) > -1) { buffer.flip(); channelOut.write(buffer); buffer.clear(); } } catch (IOException ioe) { // 当脚本执行超时,由于channel的关闭必然会抛出异常 }
// 获取执行返回值 int exitCode = process.waitFor(); if (exitCode != 0) { // 进行错误处理 } }
public void close() { process.destroy(); inputChannel.close(); } }