【问题标题】:Thread-launched running processes won't destroy (Java)线程启动的运行进程不会破坏(Java)
【发布时间】:2013-08-09 10:51:47
【问题描述】:

启动多个线程并让每个 exec() 然后destroy() 一个正在运行的 java 进程会导致某些进程没有被销毁并且在程序退出后仍在运行。这是一些重现该问题的代码。我注意到你启动的线程越多,存活的进程就越多。而且在destroy()之前睡眠越多,存活的进程就越少。 (我以 InfiniteLoop 为例。任何正在运行的进程都可以解决问题。)

编辑:已向 Oracle 报告错误,等待答复。随时分享有关该主题的任何知识/实验。

for(int i = 0; i < 100; i++)
{
  new Thread(new Runnable()
  {
    public void run()
    {
      try
      {
        Process p = Runtime.getRuntime().exec(new String[]{"java", "InfiniteLoop"});
        Thread.sleep(1);
        p.destroy();
      }catch(IOException | InterruptedException e){e.printStackTrace();}                    
    }
  }).start();
}

【问题讨论】:

  • 你有例外吗?
  • 也不例外。
  • e.printStackTrace() 在 catch 块中的执行过程中什么都不做。
  • 您是否在等待线程终止后再退出?你是从一个守护线程中生成它们(如果是这样,那么当你的程序退出时,JVM 可能会在它们的 sleep() 调用中终止它们)?
  • 我将尝试重新创建它。我是否正确假设 InfiniteLoop 只是运行一个无限循环?

标签: java multithreading process destroy


【解决方案1】:

p.destroy(); 之前使用p.waitFor();

这将确保前一个过程的完成。我认为您 p.destroy 命令比 exec() 命令执行操作更快地被调用。因此它变得无用。

【讨论】:

  • p.waitFor() 等待进程终止,所以在InfiniteLoop 的情况下,它永远不会返回。 exec() 点——我想知道;我在文档中找不到任何东西可以保证当exec() 返回时该过程实际上已经开始,尽管我认为它是因为它有能力例如找不到命令时抛出错误。
  • 不等待当前进程返回吗?然后 p.destroy 杀死进程..它在一次迭代中完成一个创建和销毁循环..
  • p.waitFor() 等待 exec() 返回,因此使用它作为标记..您可以检查 exec() 是否有效..
【解决方案2】:

如果子进程向 stdout 或 stderr 写入任何内容(有意或无意),这可能会导致麻烦:

"因为一些原生平台只提供有限的缓冲区大小 标准输入输出流,未能及时写入输入 流或读取子进程的输出流可能会导致 子进程阻塞,甚至死锁。"

来源:http://www.javaworld.com/jw-12-2000/jw-1229-traps.html

如果你需要使用 Runtime.exec(),整篇文章是 IMO 值得一读的。

【讨论】:

  • 什么都不写,只是一个空的while(true){}
【解决方案3】:

这仅仅是因为在线程执行销毁调用之前,您的主程序终止并且所有关联的线程使启动的进程继续运行。要验证这一点,只需在销毁之后添加一个 System.out 调用,您会发现它没有被执行。为了克服这个问题,在你的 main 方法的末尾添加一个 Thread.sleep 并且你不会有孤立的进程。以下不会让任何进程运行。

public class ProcessTest {

public static final void main (String[] args) throws Exception {

    for(int i = 0; i < 100; i++) {
        new Thread(new Runnable()
            {
                public void run() {
                    try {
                        Process p = Runtime.getRuntime().exec(new String[]{"java", "InfiniteLoop"});
                        Thread.sleep(1);
                        p.destroy();
                        System.out.println("Destroyed");
                    }catch(IOException e) {
                        System.err.println("exception: " + e.getMessage());
                    } catch(InterruptedException e){
                        System.err.println("exception: " + e.getMessage());
                    }
                }
            }).start();
    }


    Thread.sleep(1000);

}

}

【讨论】:

  • 这并不能解决问题;这实际上也是我运行的第一个测试之一。我有 10 秒的睡眠时间,在那段时间里,一些进程在整个睡眠期间仍然在运行,即使它们应该被销毁(并且在程序终止时也没有终止)。
  • 我在 Debian 中运行上面的代码,添加 Thread.sleep() 后没有剩余进程。我上面所做的并不是等待线程停止的正确方法。您必须在最后在循环中创建的每个线程上调用 Thread.join。你试过这个,还是一样的问题吗?
  • 试过你的代码,问题依旧。但我不明白你的意思,问题不是来自线程,而是来自进程。终止线程不会终止它启动的进程。
  • 同意,已启动的进程不会终止。但是不终止的原因有两个,不是因为没有调用destroy而终止或者destroy本身有bug。为了消除第一个问题,我建议您将主线程与已启动的线程连接起来,这样您就可以确定线程已经终止,从而消除了这种情况。调用“sleep”方法并不能消除任何场景。
  • 进一步研究显示以下结果:在 Debian "jre build 1.7.0-b147" / "jdk build 1.6.0_31-b04" 和 windows 7 32-bit "java build 1.6.0_31- b04”,进程正在正确终止。但是在使用 jdk build 1.7.0-b147 的 windows 7 64 位上,即使调用了destroy,进程也不会终止。
【解决方案4】:

您应该关闭进程的输入/输出/错误流。我们在过去看到了一些问题,即由于这些流没有关闭(即使它们没有被使用),分叉进程没有正确完成。

一个典型的解决方案:

p.destroy();
p.getInputStream().close();
p.getOutputStream().close();
p.getErrorStream().close();

【讨论】:

  • 这不应该是必要的,如果你看例如在UnixProcess#destroy()的实现中,它会自动关闭所有的流。
  • @Mifeet - 这个问题已在 2 年前得到回答。您是否正在查看两年前的 Java 生产版本? (而且这种经验可能来自早期的 jdk 6 甚至 5,在这个时间点很难让它们保持一致)。
  • 对,准确地说,我正在查看 JDK 7。
  • 即使在 Java 14 中,当所有 3 个流都未手动关闭时,Java 进程不会丢弃 Windows 上的句柄的问题仍然存在。
【解决方案5】:

我相信根据link,操作系统会产生一个不同的进程来响应这个调用。此进程的生命周期独立于您的 Java 程序和其中的线程,因此您希望它在程序退出后继续运行。我刚刚在我的机器上试了一下,它似乎按预期工作:

import java.io.*;

class Mp {
public static void main(String []args) {
    for(int i = 0; i < 100; i++) {
        new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("1");
                    Process p = Runtime.getRuntime().exec
                        (new String[]{"notepad", ""});
                    System.out.println("2");
                    Thread.sleep(5);
                    System.out.println("3");
                    p.destroy();
                    System.out.println("4");
                }
                catch(IOException | InterruptedException e) {
                    e.printStackTrace();
                }                    
            }
        }).start();
    }
}
}

【讨论】:

  • 好点 - 我忘记了销毁调用。将 e.printStackTrace() 添加到异常块以查看是否发生任何错误会很有用。
  • 向线程添加一些 println 跟踪以查看是否实际到达了 destroy() 也会很有用。
  • 我刚才在我的机器上试了一下,没有运气-我想知道OP机器上的JVM实例是否有问题。
  • @AliB 你能试试无限循环过程吗?我只遇到实际运行进程的问题。
【解决方案6】:

这不是答案;根据问题 cmets 中的讨论,我正在发布我自己尝试重现此问题的完整源代码。

我无法在 Ubuntu 12.04 上重现此问题; OpenJDK 6b_27(不过,见下文)。

ProcessTest.java:

import java.io.*;

public class ProcessTest {

    public static final void main (String[] args) throws Exception {

        for(int i = 0; i < 100; i++) {
            new Thread(new Runnable() 
                {
                    public void run() {
                        try {
                            Process p = Runtime.getRuntime().exec(new String[]{"java", "InfiniteLoop"});
                            Thread.sleep(1);
                            p.destroy();
                        }catch(IOException e) {
                            System.err.println("exception: " + e.getMessage());
                        } catch(InterruptedException e){
                            System.err.println("exception: " + e.getMessage());
                        }                    
                    }
                }).start();
        }

    }

}

无限循环.java

public class InfiniteLoop {
    public static final void main (String[] args) {
        while (true) ;
    }
}

我无法重现 JVM 终止后进程仍在运行的问题。但是,如果我在启动线程之后但在从主线程返回之前在主线程中添加一个长时间的延迟,我确实会看到大约十几个正在运行的 Java 进程仍然存在(尽管它们在主程序终止时终止)。

更新:

我只是让它在它终止后留下大约 5 个进程运行。它并不总是发生。诡异的。我也想了解更多。我有一种预感,它与过快地破坏进程或某种竞争条件有关;也许 java 分叉了一些东西或者做一些事情来创建一个新的进程,如果调用太快/在错误的时间,destroy() 不会处理。

我发现了一个旧错误(但它没有标记为已解决),指出如果一个进程产生子进程,它们可能不会被 destroy() 杀死。 bugs.sun.com/bugdatabase/view_bug.do?bug_id=4770092你用的是什么版本的JDK。

这里是另一个看起来类似问题的参考:Java tool/method to force-kill a child process 如果我只是给你的生活增添了困惑,我想道歉,我实际上并没有那么多使用 Process 并且不熟悉这些怪癖。希望其他人会介入并给出明确的答案。似乎它不能很好地处理子进程,我假设 java 分叉了一些东西。这就是我的全部。

【讨论】:

  • 我仍然遇到此代码的问题。我用 Fedora 在另一台机器上尝试过,同样的问题。但是我在虚拟盒子中尝试了Windows 7 64,没有问题!那么它与操作系统有关吗?
  • Ubuntu 不起作用,请查看我在您的问题中留下的 cmets。我随机遇到了同样的问题。
  • 为什么不将其编辑到问题中而不是将其作为答案发布?
  • 当我与 OP 来来回回时,我需要一个地方来发布我的测试代码。我觉得把它编辑成别人的问题是不合适的。一旦解决,我将删除它。我同意这并不理想,但 SO 不适合这些类型的讨论。 :)
  • 尝试在销毁调用之前添加p.getOutputStream().close(); p.getInputStream().close(); p.getErrorStream().close();
【解决方案7】:

在 Runtime.exec 启动 新线程 以启动进程与您告诉该进程自行销毁之间存在竞争条件。

我在linux机器上,所以我将使用UNIXProcess.class文件来说明。

Runtime.exec(...) 将创建一个新的 ProcessBuilder 并在 unix 机器上启动它创建一个新的 UNIXProcess 实例。在UNIXProcess 的构造函数中,有一段代码实际上在后台(分叉)线程中执行进程:

java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction() {
    public Object run() {
    Thread t = new Thread("process reaper") {
        public void run() {
                    try {
                        pid = forkAndExec(prog,
                      argBlock, argc,
                      envBlock, envc,
                      dir,
                      redirectErrorStream,
                      stdin_fd, stdout_fd, stderr_fd);
                    } catch (IOException e) {
                        gate.setException(e); /*remember to rethrow later*/
                        gate.exit();
                        return;
                    }
                    java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction() {
                        public Object run() {
                        stdin_stream = new BufferedOutputStream(new
                                                FileOutputStream(stdin_fd));
                        stdout_stream = new BufferedInputStream(new
                                                FileInputStream(stdout_fd));
                        stderr_stream = new FileInputStream(stderr_fd);
                        return null;
                    }
                    });
                    gate.exit(); /* exit from constructor */
        int res = waitForProcessExit(pid);
        synchronized (UNIXProcess.this) {
            hasExited = true;
            exitcode = res;
            UNIXProcess.this.notifyAll();
        }
        }
    };
            t.setDaemon(true);
            t.start();
    return null;
    }
});

请注意,后台线程设置了字段pid,它是UNIX 进程ID。这将被destroy() 用来告诉操作系统要杀死哪个进程。

因为在调用destroy() 时无法确保该后台线程已经运行,所以我们可能会在进程运行之前尝试将其杀死,或者我们可能会在设置pid 字段之前尝试将其杀死; pid 未初始化,因此为 0。所以我认为过早调用 destroy 将相当于 kill -9 0

UNIXProcess destroy() 中甚至有一条评论暗示了这一点,但只考虑在进程完成后调用destroy,而不是在它开始之前:

// There is a risk that pid will be recycled, causing us to
// kill the wrong process!  So we only terminate processes
// that appear to still be running.  Even with this check,
// there is an unavoidable race condition here, but the window
// is very small, and OSes try hard to not recycle pids too
// soon, so this is quite safe.

pid 字段甚至没有被标记为 volatile,因此我们甚至可能始终看不到最新的值。

【讨论】:

  • 这里不存在这种竞争条件。 UNIXProcess 构造函数直到 线程中的 forkAndExec 调用完成后才返回——这就是 gate 的用途。事实上,如果进程无法启动,这就是它能够抛出异常的方式,您会注意到异常源自该后台线程。所以在进程启动之前构造函数不会返回,因此在进程启动之前不能调用destroy()
  • 您在destroy() 中看到的评论指的是不同的场景。该评论指的是如果一个进程自行死亡,操作系统可能会将其 PID 重用于其他一些新进程,因此当我们的应用程序调用destroy() 时,PID 可能是一个不同的不相关进程,而我们不这样做'不想杀人。该评论附近的代码只是说“如果进程已经自行退出,请不要杀死它”,这是一种避免杀死错误事物的尝试(不是 100% 可靠,如上所述)。
【解决方案8】:

我遇到了一个非常相似的问题,destroy() 无法正常工作的问题即使使用单个线程也很明显。

Process process = processBuilder(ForeverRunningMain.class).start()
long endTime = System.currentTimeMillis() + TIMEOUT_MS; 
while (System.currentTimeMillis() < endTime) {
    sleep(50);
}
process.destroy();

如果TIMEOUT_MS 太低,进程并不总是被破坏。 destroy() 之前添加一个额外的sleep() 修复了它(尽管我没有解释原因):

Thread.sleep(300);
process.destroy();

【讨论】:

    猜你喜欢
    • 2011-07-21
    • 1970-01-01
    • 2015-03-22
    • 1970-01-01
    • 2021-10-17
    • 2012-06-22
    • 2011-09-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多