子线程抛出的未捕获异常,很多人只知道"控制台会打印一堆堆栈",这篇真实验证它对其他线程的影响、如何用handler拦截,以及最容易被忽视的一点——线程池里的异常根本不走这套机制。
上一篇:锯齿数组真实验证
最基础的行为验证:一个线程崩溃了,是不是会拖累整个程序。
UncaughtExceptionHandlerDemo.java Thread defaultThread = new Thread(() -> { throw new RuntimeException("子线程真实抛出的未捕获异常"); }); defaultThread.start(); defaultThread.join(); mainContinued.set(true); // main线程真实还能走到这里吗?
Exception in thread "Thread-0" java.lang.RuntimeException: 子线程真实抛出的未捕获异常 at UncaughtExceptionHandlerDemo.lambda$main$0(UncaughtExceptionHandlerDemo.java:9) at java.base/java.lang.Thread.run(Thread.java:840) 子线程抛出未捕获异常后,main线程真实继续执行到这里: true
真实结果证明了 Java 线程模型的一条基本设计:每个线程真实拥有独立的执行栈,一个线程未捕获的异常只会真实终止这一个线程,不会向上传播影响其他线程(包括创建它的线程)。控制台看到的堆栈打印,是 JVM 默认的 UncaughtExceptionHandler 实现在真实做的事——它只是把异常信息输出到 stderr,除此之外什么都不做,main 线程完全不受影响,真实继续往下执行。
用setUncaughtExceptionHandler()替换默认的"打印到stderr"行为,验证能否真实拿到异常线程和异常消息。
UncaughtExceptionHandlerDemo.java customThread.setUncaughtExceptionHandler((t, e) -> { capturedThreadName.set(t.getName()); capturedMessage.set(e.getMessage()); });
自定义handler真实捕获到的线程名: custom-worker 自定义handler真实捕获到的异常消息: 真实的业务异常信息
最容易踩坑的一点:提交到线程池的任务抛异常,是否也会像普通Thread那样打印堆栈或触发handler?
UncaughtExceptionHandlerDemo.java Future<?> future = pool.submit(() -> { throw new RuntimeException("线程池任务里的异常"); }); future.get(); // 必须调用这个才能真实拿到异常
线程池任务异常真实通过Future.get()的ExecutionException包装拿到: 线程池任务里的异常
真实结果证明了一个容易被忽视的重要区别:通过 ExecutorService.submit() 提交的任务,内部真实用 FutureTask 包装了执行逻辑,任务抛出的异常真实被 FutureTask 捕获并存了起来,不会真实触发线程的 UncaughtExceptionHandler,控制台也真实不会自动打印堆栈(这也是为什么"线程池里的任务偷偷抛了异常但完全没人发现"是真实存在的运维事故常见原因)——必须调用 future.get(),异常才会真实被重新抛出,包装成 ExecutionException,原始异常在它的 getCause() 里。这也是为什么线程池任务的异常处理应该始终检查 Future 的结果,或者用 ThreadPoolExecutor 的 afterExecute() 钩子方法主动检查。
未单独设置handler的线程抛异常,全局默认handler真实是否被触发: true
真实结果证明了 Thread.setDefaultUncaughtExceptionHandler() 设置的是一个全局兜底策略——只要某个线程没有通过 setUncaughtExceptionHandler() 单独指定自己的 handler,它未捕获的异常就真实会走到这个全局默认 handler。这在真实项目里常用于统一给所有线程(包括没有特意配置过的线程池工作线程)接上一套统一的异常上报逻辑,作为最后一道兜底防线。
每一行都是真实运行这段代码(每一步都用 join() 等待线程结束,结果是确定性的)得到的结果。