UncaughtExceptionHandler 和 System.exit()

UncaughtExceptionHandler and System.exit()

我编写了一个自定义 UncaughtExceptionHandler,它应该将异常打印到控制台并使用自定义退出代码关闭应用程序。

class 看起来像这样:

public class FatalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(final Thread t, final Throwable e) {
        System.out.println("Handled exception in " + t.getName() + ":");

        e.printStackTrace();

        System.exit(ExitCodes.UNKNOWN_EXCEPTION);
    }
}

我在 Main.class 中设置 UncaughtExceptionHandler 是这样的:

Thread.setDefaultUncaughtExceptionHandler(new FatalUncaughtExceptionHandler());

然后我生成并启动 4 个线程。

在其中一个 运行 线程中,我故意使用 Integer.valueOf("Test") 生成一个 NumberFormatException 以测试我的处理程序。这很好用;这是输出:

Handled exception in WatchdogThread:
java.lang.NumberFormatException: For input string: "Test"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Integer.parseInt(Integer.java:580)
    at java.lang.Integer.valueOf(Integer.java:766)
    at com.csg.gfms.gms.ctmgate.runnable.WatchdogThread.run(WatchdogThread.java:43)

现在我有一个问题。由于某种原因,抛出异常的线程没有被 System.exit() 命令关闭。显然我的 ShutdownHook 锁定了它。 (如 jvisualvm 的输出所示):


"WatchdogThread" #38 prio=5 os_prio=0 tid=0x000000001efa3800 nid=0xd40 in Object.wait() [0x0000000021a5e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076e30a7c0> (a com.csg.gfms.gms.ctmgate.runnable.CTMShutdownHook)
        at java.lang.Thread.join(Thread.java:1252)
        - locked <0x000000076e30a7c0> (a com.csg.gfms.gms.ctmgate.runnable.CTMShutdownHook)
        at java.lang.Thread.join(Thread.java:1326)
        at java.lang.ApplicationShutdownHooks.runHooks(ApplicationShutdownHooks.java:107)
        at java.lang.ApplicationShutdownHooks.run(ApplicationShutdownHooks.java:46)
        at java.lang.Shutdown.runHooks(Shutdown.java:123)
        at java.lang.Shutdown.sequence(Shutdown.java:167)
        at java.lang.Shutdown.exit(Shutdown.java:212)
        - locked <0x00000006c9605b00> (a java.lang.Class for java.lang.Shutdown)
        at java.lang.Runtime.exit(Runtime.java:109)
        at java.lang.System.exit(System.java:971)
        at com.csg.gfms.gms.ctmgate.handlers.FatalUncaughtExceptionHandler.uncaughtException(FatalUncaughtExceptionHandler.java:13)
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1057)
        at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1052)
        at java.lang.Thread.dispatchUncaughtException(Thread.java:1959

甚至 IntelliJ 都告诉我 System.exit 命令会失败。在调试我的 UncaughtExceptionHandler 时,它旁边会显示一个小标记,上面写着“方法将失败”。


这引出了我的问题:

是否不允许从 UncaughtExceptionHandler 调用 System.exit()?

在我的情况下是否启动了两次关闭挂钩?

关闭挂钩被锁定的原因可能是什么?

看到跟踪中的 com.csg.gfms 东西了吗?

不是java;是你。那是您的代码在另一个关闭挂钩中阻塞;一个正在调用 Thread.join.

通常当运行遇到这种怪事时,如果完全有可能制作一个stand-alone超级简单的测试用例,那么你应该这样做。我已经为你完成了:

class Test {
        public static void main(String[] args) throws Exception {
                Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                        public void uncaughtException(Thread t, Throwable e) {
                                System.out.println("EXITING");
                                System.exit(1);
                        }
                });
                for (int i = 0; i < 10; i++) {
                        Thread t = new Thread(new Runnable() {
                                public void run() {
                                        try {
                                                Thread.sleep(1000L);
                                        } catch (Exception ignore) {}
                                        throw new RuntimeException();
                                }
                        });
                        t.start();
                }
                Thread.sleep(2000L);
                System.out.println("Still alive?");
        }
}

当我 运行 这样做时,我得到任意数量的 EXITING 打印(2 到 3,这取决于有多少内核同时在这些线程上工作),然后 VM hard-exits。 Still alive? 从不打印,不发生锁定,VM 实际退出。

因此证明从未捕获的异常处理程序中调用 System.exit() 不是问题。

关闭钩子没有被调用两次;由于您调用 System.exit 而调用关闭挂钩,而不是因为我们到达了未捕获的异常处理程序。但是,如果你担心这个,嘿,这是你的应用程序,请在你的关闭挂钩中打印一些东西以确保。

锁定问题不在 关闭挂钩上。您可以注册任意数量的关闭挂钩。它位于 a 关闭挂钩中。具体来说:有人注册了 com.csg.gfms.gms.ctmgate.runnable.CTMShutdownHook 的实例,该代码正在加入某个线程,并且该线程没有关闭,因此该挂钩永远不会退出,因此 System.exit 不会退出 VM。 解决方法是修复损坏的 CTMShutdownHook

在关闭挂钩中加入一个线程是......好吧,我会直截了当地说:愚蠢。我不太清楚这是要达到什么目的,但我唯一能想到的就是强制遵守一个糟糕的标准。因此,我可以预见,您或 CTMShutdownHook 的作者首先需要对如何处理 JVM 关闭进行一些反省,以便他们了解其实现背后的想法从根本上是错误的,需要重新思考。

我会在这里做。

有这样一种心态,即要 'properly' 关闭虚拟机,永远不应该调用 System.exit,而是应该小心地告诉所有 运行ning 线程停止,并且应该仔细管理所有线程上的守护程序标志,以便 VM 最终会自行关闭,因为所有仍处于活动状态的线程都设置了守护程序标志。争论是这给了每个线程 'shut down nicely'.

的机会

这是错误的代码风格。

如果有人按下 CTRL+C 或以其他方式要求 VM 退出,您的应用程序将关闭,这不会产生一个很好的 'ask all threads to clean up and stop' 过程。事实上,如果有人绊倒电源线、计算机 hard-crashes 或有人终止应用程序,您的应用程序清理任何东西的机会为零。

这导致以下规则:

  • 任何编写的代码,如果不能很好地关闭,它就会中断(例如,你在内存中保留一些状态,当被要求退出时,你将这个状态保存到磁盘;如果这个状态被遗忘,这是一个非常严重的错误)是错误的代码。总是可以编写代码以便恢复。即使是极端情况,例如文件系统,(如今)也可以使用例如拉线来处理。日记技术。
  • 如果您想要至少 'be nice' 并尝试保存状态或以其他方式清理,请不要等待别人告诉您的线程 'exit nicely'。只需注册一个执行清理的关闭处理程序,并假设您的主线程循环将在任意点直接中止而无需任何进一步通知。这个其实不难写。
  • 换句话说:永远不要假设您的线程会被告知自行清理。假设通常会调用任何已注册的关闭处理程序,但不要完全依赖它们,如在极少数情况下(电源脉冲、kill -9、VM 核心崩溃、内存问题、有人 运行 在 IDE 并直接杀死它,通常是 hard-kill,列表很长)那些也不会 运行。

通过添加 'joins' 一个线程的 shutdownhook(加入 = 暂停该线程直到该线程退出),您创建了一个非常愚蠢的场景,其中有 3 种不同的关闭应用程序的方法:

  • 有人被电源线绊倒或 kill -9s 你的应用程序:一切都当场死亡,无法清理。
  • CTRL+C 被击中或有人调用 System.exit 或正常 SIGKILLs 你的应用程序:一切都当场死亡,但所有关闭挂钩都被调用。
  • (误入歧途)在应用程序中,一些进程开始尝试让所有 non-daemon 线程进入 return,并且它们可能会在内部进行清理。

'join this thread in a shutdown hook' 所做的是有效地将第二种形式降级为(坏的)第三种形式。

有了这个上下文,您现在可以修复 CTMShutdownHook 中损坏的代码,或者与该钩子的开发人员交谈并向他们解释 elegant-sounding 允许 all[=68] 的想法=] 运行正常关闭线程实际上很糟糕。

那么作为一个更一般的原则,shutdown hooks should block尽可能少,绝对不应该等待其他线程执行。