线程池的 submit 与 execute 有什么区别?异常去哪了?

#Java#面试八股

线程池的 submit 与 execute 有什么区别?异常去哪了?

结论先行:execute(Runnable) 没有返回值,任务抛出的异常会交给线程的 UncaughtExceptionHandler,线程随即终止并被替换;submit(...) 返回 Future,异常被 FutureTask 捕获存起来,不调 get() 就永远发现不了。这就是"线程池吞异常"的根源。

1. 逐项对比

维度execute(Runnable)submit(Callable/Runnable)
定义位置Executor 接口ExecutorService 接口
返回值voidFuture<T>
接受类型仅 RunnableRunnable 或 Callable
异常去向抛给 UncaughtExceptionHandler存进 Future.outcome
异常何时暴露立即,任务执行时调用 get() / isDone() 时
工作线程是否终止是(随后补建新线程)否(异常被 FutureTask 吃掉)
是否可取消否是(future.cancel())

2. 调用链差异

execute(task)
  └─▶ ThreadPoolExecutor.execute
        └─▶ runWorker → task.run()          ← 异常直接向上抛
              └─▶ 线程 died,抛给 UncaughtExceptionHandler
                    └─▶ 池子补一个新 worker

submit(task)
  └─▶ AbstractExecutorService.submit
        └─▶ newTaskFor(task) → FutureTask   ← 包装一层
              └─▶ FutureTask.run
                    ├── 正常:set(result)
                    └── 异常:setException(ex) 存进 outcome,不抛出
                          └─▶ 只有 future.get() 时才以 ExecutionException 抛出

3. 异常被吞的完整示例

ExecutorService pool = Executors.newFixedThreadPool(2);

// 1. execute:异常会打印到控制台(默认 handler)
pool.execute(() -> { throw new RuntimeException("execute 的异常"); });

// 2. submit:什么都不打印,异常被 FutureTask 收走了
Future<?> f = pool.submit(() -> { throw new RuntimeException("submit 的异常"); });
// 不调用 f.get(),这个异常就永远消失了

// 3. 正确姿势:显式 get() 并处理
try {
    f.get(3, TimeUnit.SECONDS);
} catch (ExecutionException e) {
    log.error("任务执行失败", e.getCause());   // 真正的异常在 cause 里
} catch (TimeoutException e) {
    f.cancel(true);
}

注意 ExecutionException 只是包装,真正的业务异常在 e.getCause() 里,日志里直接打 e 会看到两层堆栈。

4. 四种兜底手段

// 手段一:自定义线程工厂,给每个线程挂 handler
ThreadFactory factory = r -> {
    Thread t = new Thread(r, "biz-pool");
    t.setUncaughtExceptionHandler((th, ex) ->
            log.error("线程 {} 未捕获异常", th.getName(), ex));
    return t;
};

// 手段二:submit 之后统一收集 Future,遍历检查
List<Future<?>> futures = new ArrayList<>();
tasks.forEach(t -> futures.add(pool.submit(t)));
for (Future<?> f : futures) {
    try { f.get(); } catch (ExecutionException e) { log.error("失败", e.getCause()); }
}

// 手段三:重写 afterExecute,在任务结束后检查异常
ThreadPoolExecutor pool2 = new ThreadPoolExecutor(4, 8, 60, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100)) {
    @Override protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?> f && f.isDone() && !f.isCancelled()) {
            try { f.get(); } catch (ExecutionException e) { log.error("异步任务异常", e.getCause()); }
        }
    }
};

// 手段四:用 CompletableFuture,异常流可显式处理
CompletableFuture.runAsync(task, pool)
        .exceptionally(ex -> { log.error("失败", ex); return null; });
手段覆盖范围侵入性
自定义 handler只覆盖 execute 与未被 FutureTask 捕获的异常低
收集 Future.get()全覆盖,但会阻塞中
重写 afterExecute全覆盖,不阻塞,推荐中
CompletableFuture全覆盖,可编排中(需改造调用方式)

5. 常见追问

追问一:为什么 submit 要吞异常? 因为 Future 的语义就是"结果稍后取"。异常也是一种"结果",必须保存在 Future 里等调用方来取。如果直接抛,调用方就永远拿不到这个异常了。

追问二:execute 抛异常后线程会怎样? 工作线程终止,线程池检测到 worker 退出后会补建新线程。但如果异常频繁发生,会出现"线程不断创建销毁"的现象,poolSize 指标会异常抖动。

追问三:CallerRunsPolicy 下 submit 的异常去哪? 任务被提交线程直接执行,此时 FutureTask.run 依然会捕获异常存进 outcome,所以还是"不 get 不知道"。背压场景下这一点尤其危险。

追问四:Future.cancel(true) 能中断正在执行的任务吗? 能,但前提是任务内部响应中断(检查 isInterrupted() 或调用可中断方法)。cancel 只是设置中断标志,不能强行停止线程。

小结

一句话记:execute 会喊出来,submit 会咽下去。生产代码要么统一用 afterExecute 做异常兜底,要么用 CompletableFuture 显式挂 exceptionally,千万不要"submit 完就不管了"——那等于把线上错误静默丢进黑洞。

笔记加载中…