java子线程未捕获异常默认被静默吞掉,不会打印日志或终止程序;需通过thread.setdefaultuncaughtexceptionhandler全局兜底或setuncaughtexceptionhandler单线程设置来捕获,executorservice中则须用callable+future.get()显式获取异常。

Java线程中未捕获的异常会直接丢失
主线程抛出未捕获异常会打印堆栈并终止程序,但子线程不是这样:Thread 默认对未捕获异常不做任何处理,异常对象被静默吞掉,连日志都不会输出。这是很多线上问题排查不到的根本原因。
根本原因是 JVM 不会自动将子线程的异常传播回启动它的线程,也不会中断其他线程。除非显式干预,否则 run() 方法里抛出的 RuntimeException 或错误(如 NullPointerException)就“消失”了。
用 Thread.setDefaultUncaughtExceptionHandler 全局兜底
这是最常用、也最推荐的第一道防线,适用于所有未显式设置异常处理器的线程:
- 在
main()方法开头或应用初始化阶段调用一次即可生效 - 它只影响后续新创建的线程,不影响已启动的线程
- 处理器函数接收两个参数:
Thread实例和Throwable异常对象 - 务必记录完整堆栈(用
e.printStackTrace()或日志框架的error(..., e)),否则等于没做
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("Uncaught exception in thread " + t.getName() + ": " + e.getMessage());
e.printStackTrace();
});
给单个 Thread 设置 setUncaughtExceptionHandler
当需要为某类任务定制异常行为(比如重试、告警、清理资源),而不是全局统一处理时,应直接在 Thread 实例上调用:
立即学习“Java免费学习笔记(深入)”;
- 优先级高于全局处理器:如果线程自己设置了处理器,就不会走
setDefaultUncaughtExceptionHandler - 注意:必须在
start()之前设置,start()之后调用无效 - 适合与
Runnable解耦——异常处理逻辑封装在线程对象上,不侵入业务代码
Thread t = new Thread(() -> {
throw new RuntimeException("boom");
});
t.setUncaughtExceptionHandler((thread, ex) -> {
System.err.println("Custom handler for " + thread.getName());
// 比如发钉钉告警、记录到监控系统
});
t.start();
ExecutorService 中的异常更隐蔽,不能只靠 UncaughtExceptionHandler
使用 ExecutorService 提交 Runnable 时,异常依然会被吞掉;但提交 Callable 并调用 Future.get() 才能真正暴露异常。这是最容易踩坑的地方:
-
executor.submit(runnable)→ 异常进UncaughtExceptionHandler(如果设置了) -
executor.submit(callable)→ 异常被包装成ExecutionException,必须显式调用future.get()才能拿到 - 若忘记调用
get(),异常依旧丢失,且线程池可能继续执行后续任务 - 建议:对关键任务一律用
Callable,并在获取结果时做 try-catch
Future<String> f = executor.submit(() -> {
throw new RuntimeException("in callable");
});
try {
f.get(); // 这里才真正抛出 ExecutionException
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // 原始异常在这里
log.error("Task failed", cause);
}
真正麻烦的不是怎么写 handler,而是很多人根本没意识到线程异常默认不报错。一旦用了线程池又没检查 Future,或者忘了设全局 handler,问题就藏在生产环境里等你花三天查内存泄漏。







