未捕获异常会终止线程且不传播至主线程;Runnable任务异常无法通过Future.get()获取,需改用Callable;ExecutorService中应避免shutdownNow()截断异常;ForkJoinPool支持异常冒泡,CompletableFuture需正确选用exceptionally/whenComplete/handle。

未捕获的异常会直接终止线程,且不会传播到主线程
Java 中 Thread 默认对未捕获异常的处理是:打印堆栈后静默退出。这意味着如果子线程抛出 RuntimeException(如 NullPointerException、ArrayIndexOutOfBoundsException),主线程完全感知不到,也不会中断或失败——这极易掩盖逻辑缺陷或资源泄漏问题。
- 不要依赖
try-catch包裹run()全部逻辑来“兜底”,它只解决当前线程,不解决异常传递与统一响应问题 - 若需主线程感知子线程异常,必须显式设计通信机制(如共享
AtomicReference或使用Future.get()) -
Thread.setDefaultUncaughtExceptionHandler()可设全局兜底处理器,但仅适用于未被任何catch捕获的异常,且每个线程可单独设置,优先级高于默认值
ExecutorService 中的 Runnable 任务无法通过 get() 获取异常
提交 Runnable 到 ExecutorService(如 Executors.newFixedThreadPool(2))时,返回的是 Future> ,调用 future.get() 永远返回 null,即使任务内部抛了异常——因为 Runnable 没有返回值,也不声明异常,JVM 不会把异常封装进 Future。
- 改用
Callable提交任务,其call()方法允许抛异常,且Future.get()会在异常发生时抛出ExecutionException,原始异常可通过e.getCause()获取 - 若必须用
Runnable,可在任务内手动捕获并写入共享状态(如ConcurrentLinkedQueue),再由主线程轮询检查 - 注意:
ExecutorService.shutdownNow()不会等待正在运行的任务完成,异常可能被截断,应配合awaitTermination()使用
ForkJoinPool 的异常传播行为与普通线程池不同
ForkJoinPool 执行 RecursiveAction 或 RecursiveTask 时,子任务异常默认会“向上冒泡”到 join() 调用点,但仅限于同一线程池内的父子任务链;跨池或外部线程调用 join() 时,仍需处理 ExecutionException。
-
RecursiveTask的compute()抛异常 → 调用fork().join()的地方收到ExecutionException,getCause()即原始异常 - 若使用
invoke()(同步执行),异常会直接抛出,无需get()封装 - 避免在
compute()中吞掉异常(如空catch),否则会导致任务静默失败,且isCompletedAbnormally()返回true但无日志线索
CompletableFuture 的异常处理要区分 whenComplete / handle / exceptionally
这三个方法都用于响应异常,但语义和触发时机完全不同,误用会导致异常被忽略或重复处理:
立即学习“Java免费学习笔记(深入)”;
-
exceptionally(Function:仅在前序阶段抛异常时触发,返回替代值;若前序成功,该函数不执行) -
whenComplete(BiConsumer super T, ? super Throwable>):无论成功或异常都会执行,但不能修改结果(参数是void),适合记录日志或清理资源 -
handle(BiFunction super T, ? super Throwable, ? extends R>):总执行,可返回新结果,能同时处理成功值和异常,但要注意判空Throwable
典型陷阱:thenApply 后接 exceptionally,但如果 thenApply 内部又抛新异常,这个新异常会被下一个 exceptionally 捕获——链式调用中每层异常都只被紧邻的异常处理器捕获。








