概述Java 提供两种方式调用外部系统命令或可执行程序Runtime.getRuntime().exec()和ProcessBuilder。Runtime.exec是早期API写法简单但坑较多ProcessBuilder是JDK1.5引入生产环境推荐优先使用支持自定义工作目录、环境变量参数传递更安全。绝大多数线上出现的服务假死、线程阻塞、定时任务堆积问题根源都是对子进程输出缓冲区机制不了解。底层原理当Java通过ProcessBuilder/Runtime.exec创建子进程时操作系统会为子进程分配两块独立缓冲区标准输出stdout、标准错误stderr。子进程打印日志会写入对应缓冲区如果Java主线程不去消费缓冲区的数据缓冲区写满后子进程会被操作系统阻塞无法继续执行、无法退出waitFor()永久阻塞线程被挂死。真实线上卡死案例案例背景后端文件转码服务使用FFmpeg做视频抽帧本地单元测试全部正常上线后随机出现任务卡死。现象大部分任务正常少量任务永久不结束线程池工作线程被占满新任务排队超时最终服务接口大面积超时只能重启服务临时恢复。事故原始错误代码publicclassFFmpegDemo{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{StringffmpegPath/usr/bin/ffmpeg;ProcessBuilderpbnewProcessBuilder(ffmpegPath,-i,input.mp4,frame.jpg);Processprocesspb.start();// 只等待进程结束完全不读取stdout和stderr输出process.waitFor();System.out.println(任务完成);}}根因分析FFmpeg在处理异常视频文件时会持续输出大量错误日志快速填满stderr缓冲区。代码没有读取流缓冲区满FFmpeg进程被操作系统阻塞停住waitFor()一直等待子进程退出线程永久占用反复出现后线程池耗尽。同类高发场景调用git、mvn、解压工具、PDF转换工具这类程序容易产生大量日志极易触发缓冲区满阻塞。修复方案使用redirectErrorStream(true)将错误流合并到标准输出流统一读取输出清空缓冲区。publicclassFFmpegDemo{publicstaticvoidmain(String[]args)throwsIOException,InterruptedException{StringffmpegPath/usr/bin/ffmpeg;ProcessBuilderpbnewProcessBuilder(ffmpegPath,-i,input.mp4,frame.jpg);// 合并错误流到标准输出流避免单独stderr缓冲区堆积pb.redirectErrorStream(true);Processprocesspb.start();// 读取输出清空缓冲区try(BufferedReaderbrnewBufferedReader(newInputStreamReader(process.getInputStream()))){Stringline;while((linebr.readLine())!null){System.out.println(line);}}process.waitFor();System.out.println(任务完成);}}ProcessBuilder 通用生产可用工具方法增加超时控制防止极端情况下线程永久阻塞捕获异常并强制销毁进程。importjava.io.BufferedReader;importjava.io.InputStreamReader;importjava.nio.charset.StandardCharsets;importjava.util.concurrent.TimeUnit;publicclassCmdExecutor{/** * 执行外部命令/可执行程序 * param cmd 命令数组 * return 命令输出结果 * throws Exception 超时、执行异常 */publicstaticStringexecute(String...cmd)throwsException{ProcessBuilderpbnewProcessBuilder(cmd);pb.redirectErrorStream(true);Processprocesspb.start();StringBuilderoutputnewStringBuilder();try(BufferedReaderbrnewBufferedReader(newInputStreamReader(process.getInputStream(),StandardCharsets.UTF_8))){Stringline;while((linebr.readLine())!null){output.append(line).append(\n);}}// 超时控制30秒未完成则强制终止进程booleanfinishedprocess.waitFor(30,TimeUnit.SECONDS);if(!finished){process.destroyForcibly();thrownewException(命令执行超时强制销毁子进程);}intexitCodeprocess.exitValue();if(exitCode!0){thrownewException(命令执行失败退出码exitCode输出output);}returnoutput.toString();}publicstaticvoidmain(String[]args)throwsException{// Linux示例Stringres1execute(df,-h);System.out.println(res1);// Windows内置cmd命令需要cmd /cStringres2execute(cmd,/c,ipconfig);System.out.println(res2);// 调用外部exe程序// execute(D:/tools/convert.exe, source.pdf, target.png);}}Runtime.exec 简单示例不推荐新项目使用importjava.io.BufferedReader;importjava.io.InputStreamReader;publicclassRuntimeCmdDemo{publicstaticvoidmain(String[]args)throwsException{ProcessprocessRuntime.getRuntime().exec(cmd /c ipconfig);BufferedReaderbrnewBufferedReader(newInputStreamReader(process.getInputStream(),GBK));Stringline;while((linebr.readLine())!null){System.out.println(line);}process.waitFor();}}Runtime.exec缺点字符串命令拆分容易出现意外、参数拼接易引发命令注入不推荐新项目。关键避坑要点缓冲区阻塞stdout、stderr两个缓冲区都要处理要么合并流读取要么开启两个独立线程分别读取两个流。只要有一个缓冲区不消费就有卡死风险。内置系统命令Windows的dir、echo等属于cmd内置命令必须使用cmd /cLinux shell内置命令需要sh -c。命令注入风险禁止直接拼接用户输入字符串构造命令使用ProcessBuilder可变参数数组传参底层自动转义参数。超时控制必不可少外部程序可能死循环、挂起waitFor()一定要设置超时时间超时调用destroyForcibly()强制销毁子进程避免线程泄漏。编码问题Windows控制台默认GBK编码Linux默认UTF-8读取输入流时手动指定字符集防止输出乱码。