未捕获异常会终止线程且不传播至主线程;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 捕获的异常,且每个线程可单独设置,优先级高于默认值提交 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 执行 RecursiveAction 或 RecursiveTask 时,子任务异常默认会“向上冒泡”到 join() 调用点,但仅限于同一线程池内的父子任务链;跨池或外部线程调用 join() 时,仍需处理 ExecutionException。
RecursiveTask 的 compute() 抛异常 → 调用 fork().join() 的地方收到 ExecutionException,getCause() 即原始异常invoke()(同步执行),异常会直接抛出,无需 get() 封装compute() 中吞掉异常(如空 catch),否则会导致任务静默失败,且 isCompletedAbnormally() 返回 true 但无日志线索这三个方法都用于响应异常,但语义和触发时机完全不同,误用会导致异常被忽略或重复处理:
exceptionally(Function) :仅在前序阶段抛异常时触发,返回替代值;若前序成功,该函数不执行whenComplete(BiConsumer super T, ? super Throwable>):无论成功或异常都会执行,但不能修改结果(参数是 vo
id),适合记录日志或清理资源handle(BiFunction super T, ? super Throwable, ? extends R>):总执行,可返回新结果,能同时处理成功值和异常,但要注意判空 Throwable
典型陷阱:thenApply 后接 exceptionally,但如果 thenApply 内部又抛新异常,这个新异常会被下一个 exceptionally 捕获——链式调用中每层异常都只被紧邻的异常处理器捕获。