关于Runtime.getRuntime().exec()产生阻塞的2个陷阱
本文来自网易云社区
背景
相信做java服务端开发的童鞋,经常会遇到Java应用调用外部命令启动一些新进程来执行一些操作的场景,这时候就会使用到Runtime.getRuntime().exec(),然而这个方法如果不谨慎很容易掉进陷阱。
我们的一个PDF转码服务就踩到了这个坑掉进陷阱,这个转码服务主要是对pdf进行加密和转码成swf。这个服务上线后大部分时间都是稳定运行的,但是隔一段时间就会死掉,然后人肉手动重启一下服务就复活了。看了日志,有时候有一堆关于pdf转码过程的错误日志,有时候死掉的时候什么日志也没输出。这时候猜测可能是pdf转码异常导致应用挂掉的{因为这个转码服务一直是单线程在工作}。更深的原因大家也空没去找。反正运营反馈上传的pdf一直处在转码中很久了,一两天了还在转码中,于是开发就手动重启下服务。是的你没看过,就是一两天才发现,我们的业务监控没作上去,因为相对迭代任务,这都算不紧急的事情了。
后来运营反馈pdf问题次数增多了,于是写了个脚本,定时去检查日志最后的更新时间,发现日志超过一个小时没更新就重启应用,重启脚本没问题,问题是应用重启后,日志中出现了一堆的找不到要执行的命令。目前也不知道为什么通过脚本去重启动应用后,应用找不到要执行的命令。有知道的可以告知下。
终于某一天,应用又死掉了,看了下数据库堆积了将近2000个待转的文件。看了下应用日志打了exe()后就再也没内容了,于是下狠心花了半天时间来研究下Runtime.getRuntime().exe()找了下原因,最终解决了这个问题。
关于Runtime.getRuntime().exe()
根据jdk官方文档描述,每个Java应用都存在一个而Runtime的单例实例。这个类Runtime类封装了应用运行时的环境,通过这个类我们的java应用可以与其运行环境相连接。
1、java应用无法创建自己的Runtime实例,只能通过Runtime.getRuntime()来取得当前JVM的运行时环境,这也是在Java中唯一一个得到运行时环境的方法。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法来控制Java虚拟机的状态和行为。
2、Runtime中的exit方法是退出当前JVM的方法,System类中的exit实际上也是通过调用Runtime.exit()来退出JVM的,这里说明一下Java对Runtime返回值的一般规则(后边也提到了),0代表正常退出,非0代表异常中止。
3、Runtime具有的详细方法请参考官方api,http://docs.oracle.com/javase/8/docs/api/。
阻塞陷阱之Runtime.getRuntime().exe()的返回值Process
应用在调用Runtime.getRuntime().exec()这个方法会创建一个本机进程并返回Process子类的一个实例。该实例可用来控制该进程并获得其相关信息。Process类提供了执行从进程输入、执行输出到进程、等待进程完成、检查进程的退出状态以及销毁(杀掉)进程的方法。
官方文档解释了创建进程的方法可能无法针对某些本机平台上的特定进程很好地工作,比如,本机窗口进程,守护进程,Microsoft Windows 上的 Win16/DOS 进程,或者 shell脚本。创建的子进程没有自己的终端或控制台。它的所有标准 io(即 stdin、stdout 和 stderr)操作都将通过三个管道重定向到父进程(也就是调用者java应用)。三个管道用于处理标准输入流,标准输出流,标准错误流。子进程在执行过程中,会不断的向JVM写入标准输出和标准错误输出。java应用可以通过Process 提供的getOutputStream()、getInputStream() 和 getErrorStream()来获得子进程输入输出信息。因为有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,当标准输出或者标准错误输出写满缓存池时,程序无法继续写入,子进程无法正常退出。读写子进程的输出流或输入流迅速出现失败,则可能导致子进程阻塞,甚至产生死锁。
当调用Runtime.getRuntime().exe()后返回的Process对象除了可以多的三种输入输出流外,还有两个常用的方法:
1、非阻塞方法exitValue()获得子进程退出的状态值(0,正常退出,非0异常退出),需要注意的是调用这个方法程序会立即得到结果,如果子进程没有执行完,调用这个方法会抛出IllegalThreadStateException,表示此 Process 对象表示的子进程尚未终止。
2、阻塞方法 waitFor()导致当前线程等待,直到子进程结束并返回退出状态。如果已终止该子进程,此方法立即返回,如果没有终止该子进程,调用的线程将被阻塞,直到退出子进程。
先看看我们转码服务这里的历史代码:
这段代码,用同步的方法去读取标准错误输出流即相当于清空了错误输出流缓冲区,然而正常的标准输出流并没有清空,按照上面的原理解释,阻塞的原因可能就产生在这里。当阻塞产生的时候jstack了一下线程栈信息如下图所示。确实线程锁在了读取缓冲流上面了。
这种情况网上通用的解决方法就是异步开两个线程去读取正常的输出和错误输出流信息,清空缓冲区,参考了大家的解决方法,下图是修改后的方案,ProcessClearStream是一个异步线程,主要做的是将标准inputSream读取完毕。
阻塞陷阱之子进程阻塞
通过上面的代码优化后还是发现有转码阻塞的现象出现,而且发现每次阻塞都出现在固定的几个pdf上,测试发现重启应用后主要转到那几个特定的pdf时候,转码服务必挂无疑(通常一个pdf转码只需要几十秒,而这个阻塞持续几个小时,不人为干预它就可能无限阻塞下去)。所以重启应用也不管用了,只能跳过这几个pdf应用才行,于是在测试环境测试这几个pdf,每次阻塞的时候再jstack发现应用阻塞在proc.waitFor(),再也没其他错误信息了。查看了官方api,Process的waitFor方法本身会阻塞直到子进程正常或异常退出,到这里,应该可以推断是子进程无限阻塞下去了,导致waitFor一直阻塞中。为了验证这个推断,直接在终端kill掉这个子进程,然后再查看日志,发现转码服务又继续工作了。
有了上面的结论,一个简单的思路也就有了,我需要检测子进程状态,如果发现子进程有阻塞状态就kill掉(因为这个转码脚本比较老,要拿他的堆栈信息比较麻烦,所以kill掉是最简单直接暴力效率高的方法)。将这个想法和同事聊了下,万能的Java肯定可以干这事,大概思路就启动个线程去监控process的waitFor的阻塞时间,超过设置时间,就干掉了子进程,这不是Java线程池ExecutorService类配合Future接口来干的事情么。同事按照这个思路网上找了下现成的代码,于是照着这个这个方法抄袭了一下,下面贴下关键的代码:
当waitFor超时线程中断的的时候再调用process的destroy()销毁子进程。这个方案上线后,截至目前一周多时间转码服务稳定运行,没在出现以前的服务死掉的情况。我们业务中当检测到超时退出后就重置任务状态为失败(算是降级吧),导致这种pdf转码子进程阻塞的一般是pdf本身不太标准,而这个转码工具不能很好的兼容处理这些pdf,后面把这些有问题的pdf重新转成标准pdf上传测试即可以正常转码。
参考资料
http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html
http://www.cnblogs.com/BeautyConcurrency/p/4108196.html
https://segmentfault.com/a/1190000000372535
网易云产品免费体验馆,无套路试用,零成本体验云计算价值。
本文来自网易实践者社区,经作者潘胜一授权发布
相关文章:
【推荐】 深入解析SQLServer高可用镜像实现原理
【推荐】 限时购校验小工具&dubbo异步调用实现限
关于Runtime.getRuntime().exec()产生阻塞的2个陷阱的更多相关文章
- Runtime.getRuntime().exec 类 防止阻塞
import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; impor ...
- 用Runtime.getRuntime().exec()需要注意的地方
有时候我们可能需要调用系统外部的某个程序,此时就可以用Runtime.getRuntime().exec()来调用,他会生成一个新的进程去运行调用的程序. 此方法返回一个java.lang.Proce ...
- 使用Runtime.getRuntime().exec()方法的几个陷阱 (转)
Process 子类的一个实例,该实例可用来控制进程并获得相关信息.Process 类提供了执行从进程输入.执行输出到进程.等待进程完成.检查进程的退出状态以及销毁(杀掉)进程的方法. 创建进程的方法 ...
- [转]java调用外部程序Runtime.getRuntime().exec
Runtime.getRuntime().exec()方法主要用于执行外部的程序或命令. Runtime.getRuntime().exec共有六个重载方法: public Process exec( ...
- [转]使用Runtime.getRuntime().exec()方法的几个陷阱
Process 子类的一个实例,该实例可用来控制进程并获得相关信息.Process 类提供了执行从进程输入.执行输出到进程.等待进程完成.检查进程的退出状态以及销毁(杀掉)进程的方法. 创建进程的方法 ...
- 使用Runtime.getRuntime().exec()方法的几个陷阱
Process 子类的一个实例,该实例可用来控制进程并获得相关信息.Process 类提供了执行从进程输入.执行输出到进程.等待进程完成.检查进程的退出状态以及销毁(杀掉)进程的方法. 创建进程的方法 ...
- Runtime.getRuntime().exec()需要注意的地方
文章出处http://www.cnblogs.com/fclbky/p/6112180.html 有时候我们可能需要调用系统外部的某个程序,此时就可以用Runtime.getRuntime().exe ...
- Android: 通过Runtime.getRuntime().exec调用底层Linux下的程序或脚本
Android Runtime使得直接调用底层Linux下的可执行程序或脚本成为可能 比如Linux下写个测试工具,直接编译后apk中通过Runtime来调用 或者写个脚本,apk中直接调用,省去中间 ...
- Runtime.getRuntime().exec方法
Runtime.getRuntime().exec()方法主要用于执行外部的程序或命令. Runtime.getRuntime().exec共有六个重载方法: public Process exec( ...
随机推荐
- IDEA使用SpringBoot 、maven创建微服务的简单过程
使用IDEA新建一个简单的微服务 1. 打开IDEA,File -> New -> project 打开如下图1-1所示的对话框 图 1-1 2.点击"Next"按钮 ...
- 使用pandas进行数据预处理01
数据预处理有四种技术:数据合并,数据清洗,数据标准化,以及数据转换. 数据合并技术:(1)横向或纵向堆叠合数据 (2)主键合并数据 (3)重叠合并数据 1.堆叠合并数据: 堆叠就是简单的把两个表拼接在 ...
- springcloud-知识点总结(二):Ribbon&Feign
1.Ribbon简介 前面讲了eureka服务注册与发现,但是结合eureka集群的服务调用没讲. 这里的话 就要用到Ribbon,结合eureka,来实现服务的调用: Ribbon是Netflix发 ...
- css之标签选择器
标签(空格分隔): 标签选择器 选择器定义: 在一个HTML页面中会有很多很多的元素,不同的元素可能会有不同的样式,某些元素又需要设置相同的样式,选择器就是用来从HTML页面中查找特定元素的,找到元素 ...
- WMS接口平台配置培训
供应链管理平台地址:https://twms.ninestargroup.com/ibus/#/processconfig?scShortcutld=3_17__1_303 WMS提供WSWMS固定的 ...
- Python设计模式 - UML - 用例图(Use Case Diagram)
简介 用例图主要是从用户的角度出发对软件产品的功能及执行者进行描述的. 用例图是从需求分析到软件交付的第一步,图示化展示参与者与参与者之间.参与者与用例之间.用例与用例之间的关系,帮助开发人员更好的理 ...
- mysql数据库导入与导出
导出 导出数据和表结构: mysqldump -u用户名 -p 数据库名 > 数据库名.sql mysqldump -uroot -p dbname > dbname .sql ...
- Java高级
1.GC是什么?为什么要有GC? GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供 ...
- node.js中 koa 框架的基本使用方法
一.安装 koa npm install koa --save 二.简单使用 const koa = require('koa'); //注意使用koa需要new,跟express有点不同 let a ...
- jQuery的介绍
01-jQuery的介绍 1.为什么要使用jQuery 在用js写代码时,会遇到一些问题: window.onload 事件有事件覆盖的问题,因此只能写一个事件. 代码容错性差. 浏览器兼容性问题 ...