JVM在守护线程未完成前不会退出。

7

我刚刚发现守护线程存在一些奇怪的行为,这个问题我无法解释。我将代码精简至最小,以便验证:

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

    Thread runner = new Thread(() -> {

        final int SIZE = 350_000;
        for (int i = 0; i < SIZE; i++) {
            for (int j = i + 1; j < SIZE; j++) {
                if (i*j == SIZE * SIZE - 1) {
                    return;
                }
            }
        }
    });

    runner.setDaemon(true);
    runner.start();

    // Thread.sleep(1000);
    System.out.println("Exiting.");
}
< p > runner线程执行的代码在我的电脑上需要大约12秒才能终止,由于我只需要花一些时间计算,所以我们对它做的事情不感兴趣。< /p> < p > 如果按照原样运行此片段,则按预期工作:它在启动后立即终止。如果取消注释Thread.sleep(1000)行并运行程序,则它将工作约12秒,然后打印出“Exiting”并终止。< /p> < p > 就我了解的守护线程的工作方式而言,由于唯一运行的用户线程是使用main()方法启动的那个(runner是后台守护线程),因此一旦过去1000毫秒,它就到达了执行结束,并且JVM应该停止。此外,“Exiting”仅在12秒后打印出来,而不是程序启动时,这看起来非常奇怪。< /p> < p > 我错了吗?如何实现所需的行为(暂停一秒钟,然后停止,与runner线程正在做的无关)?< /p> < p > 我在Linux系统上使用64位Oracle JDK 1.8.0_112,在IDE或命令行中启动都有相同的行为。< /p> < p > 谢谢,Andrea< /p>

就我个人而言,在我的电脑上(Windows,JDK 1.8.0_31),这个程序可以正常运行。你的电脑是多核处理器吗? - user2189998
2
调度程序可以决定在恢复主线程执行之前运行守护线程12秒钟。你观察到的行为没有任何禁止。 - David Schwartz
@user2189998 是的,我使用的是英特尔 i7。 - Andrea Iacono
2
@David Schwartz:我同意你的观点,这种行为是合法的,但我真的希望任何JVM都能像这样表现(不在12秒内切换任何线程)! - Andrea Iacono
@DavidSchwartz 如果你想要技术上来说,一个执行 'main' 方法只输出 "hello world" 的 JVM,如果它占用全部 CPU 核心 100%,然后打印 "hello world",都是符合规格的。但很明显这是个 bug。虽然这个 bug 不像前面那么极端,但同样存在:该程序执行所需的时间比它应该花费的时间多几个数量级。 - yshavit
显示剩余5条评论
2个回答

5

这可能是计数循环优化的结果,它从您的嵌套循环中删除了安全点轮询。请尝试在JVM启动选项中添加-XX:+UseCountedLoopSafepoint标志。


2

Thread#sleep(long)可以让主线程在返回其主方法之前暂停(即在JVM认为程序已完成时,只要没有非守护线程存活)。然后调度程序就可以运行任何其他可运行的线程,这将是守护线程。目前为止,在守护线程完成执行之前,似乎没有明显的原因让JVM强制抢占守护线程以继续在主线程中执行(如果它已经醒来了),因此JVM可以继续进行调度。但是,它可能随时决定暂停正在运行的线程并安排另一个可运行的线程执行,因此您的示例不保证可重现性。

您可以通过在循环中插入对Thread#yield()#sleep(1)的调用来强制抢占。我敢打赌,你会开始看到代码段更快地退出并在完成循环之前结束。

有关线程状态和调度的更多信息,请参见此处的简要概述。

更新评论:

我无法修改后台线程中的代码(这是一项要求),因此我正在寻找一种在它花费太长时间时停止它的方法(我所做的事情的描述是stackoverflow.com/questions/41226054/…)。

只有从内部停止正在运行的线程才是合法的,因此您通常会使其在每次迭代时测试中止条件,如果满足条件,则run 方法return;。中止条件可能仅是一个布尔标志,在外部设置此标志(! 易变性警告!)。因此,最简单的解决方案是让主线程在睡眠后设置这样的标志。

另一种可能性是使用支持超时的ExecutorService,请参见 Q&A,其中包括ScheduledExecutorService的示例。

我仍然不明白调度程序如何决定在运行System.out指令之前等待12秒。

它不会等待12秒钟,而是让守护线程运行到完成,因为只有在决定是否安全停止JVM时,它才���心它是一个守护进程。对于调度程序来说,只有线程的状态很重要,就它的观点而言,在主线程睡眠1秒后,它具有正在运行(守护)和可运行线程(主线程),并没有任何迹象表明应该暂停正在运行的线程以支持可运行的线程。切换线程在计算上也很昂贵,因此如果没有任何迹象,调度程序可能会不情愿地进行切换。指示切换的方法可能是休眠和yield,但还包括GC运行和许多其他事情。


我无法在后台线程中修改代码(这是一个要求),因此我正在寻找一种方法,如果它运行时间过长就停止它(我正在做的事情的描述在 https://stackoverflow.com/questions/41226054/killing-a-java-thread-in-test)。但我仍然不明白调度程序如何决定在运行 System.out 指令之前等待 12 秒钟。 - Andrea Iacono
@AndreaIacono 我已经相应地更新了我的答案。这样清楚明白了吗? - hiergiltdiestfu
使用ExecutorService的行为与我已经尝试过的相同。对于12秒的等待时间:我已经使用线程多年了,通常调度程序会在毫秒或几十毫秒后使它们切换;在这种情况下,切换发生比平常晚1000倍以上,这对我来说似乎很奇怪。事实上,我刚刚尝试了JDK 7和6,这种情况并没有发生:System.out在sleep()之后正确地打印,而不是在运行线程结束时打印。但即使使用这些JDK,守护线程在其整个执行期间仍然保持活动状态,即使用户线程终止。 - Andrea Iacono
一个进程中的所有线程必须相互协作。如果您无法修改一个不合作的线程,那么您必须将其隔离到一个单独的进程中。 - David Schwartz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接