首发于 Java圈子

踩坑之Java执行Linux命令死锁阻塞挂起

1 问题背景

最近在做一个需求需要调用 linux 下的某个脚本来对ai的模型进行训练,很简单的需求,我像往常一样写下如下的代码片段:

Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();

But当我运行代码时,发现代码执行到waitFor后阻塞住了,我以为这也许是训练模型需要时间,毕竟是同步阻塞的方法,那我就等一会吧!一分钟……两分钟……10分钟……我渐渐的意识到,这TM不对劲啊!于是我通过以下命令查询了下linux执行情况。

ps aux |grep xxx.sh

输出的结果显示该脚本当前还在运行,而脚本的日志并没有从 inputstream 进行输出。我渐渐的慌了!

2 问题分析

2.1 观看JDK文档

根据查看JDK的文档,我看到 Process.waitFor 可能导致死锁?!仔细观看JDK的文档说明,发现文档上已经对死锁的情况进行了分析。

如果InputStream使用的buffer缓冲区有限的话,可能会导致阻塞和死锁

2.2 死锁原因

经过查阅资料,发现

1. 当我们使用 Runtime.exec 执行命令时,JAVA的线程会创建一个子进程,用于执行命令,而且子进程和JAVA线程会分别独立运行。

2. JAVA线程需要等待命令的执行完成,对命令的 日志 返回值 进行处理,所以我们在JAVA线程中调用 Process.waitFor 挂起来等待子进程完成。

3. 子进程执行时,不断的打印日志信息,我们通过 Process.getInputStream Process.getErrorStream 进行获取 正常输出日志 错误日志 进行处理。

4. 这个时候子进程不断的向JAVA线程写入数据,而JAVA线程调用 Process.waitFor 后已经阻塞挂起,而子进程在不断的向JAVA线程进行写入数据,当我们的 Process.getInputStream 的buffer缓冲区被写满,而JAVA线程依然挂起并未消费buffer中的数据,导致子进程无法继续向buffer缓冲区中继续写入数据,导致子进程也挂起。

5. 这个时候JAVA线程和子进程都处于挂起的状态,JAVA线程等待子进程的结束,子进程等待JAVA线程对buffer缓冲区中的数据进行消费。两者在相互等待导致 死锁

2.2.1 死锁原理图


3 解决方案

3.1 解决方案的思路

既然是由于buffer缓冲区的数据没有消费导致子进程挂起,那么我们从这里下手。1. 消费buffer缓冲区中的数据 2. 当JAVA线程调用 Process.waitFor 后,线程会进行挂起,那我们就使用多线程进行消费数据。

3.2 正常流程图



3.3 代码实现

下面写了一个代码实现的demo,你可以根据自己的实际情况来重写。

public class ExecUtils {
    private static final Logger logger = LoggerFactory.getLogger(ExecUtils.class);
    private static ThreadPoolExecutor executor;
    static {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("demo-pool-%d").build();
        //根据实际情况创建线程池
        executor = new ThreadPoolExecutor(6, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    public static boolean exec(String cmd) throws IOException, InterruptedException {
        String[] cmds = {"/bin/sh",
                "-c", cmd};
        Process process = Runtime.getRuntime().exec(cmds);
        //消费正常日志
        clearStream(process.getInputStream());
        //消费错误日志
        clearStream(process.getErrorStream());
        //i为返回值,判断是否执行成功
        int i = process.waitFor();
        if (i != 0) {
            logger.error("Failed to call shell command and the return status's is: {}", i);
            return false;
        return true;
    private static void clearStream(InputStream stream) {
        //处理buffer的线程
        executor.execute(new Runnable() {
            @Override
            public void run() {
                String line = null;
                try (BufferedReader in = new BufferedReader(new InputStreamReader(stream));) {
                    while ((line = in.readLine()) != null) {
                        logger.debug(line);
                } catch (IOException e) {