线程池的 submit 与 execute 有什么区别?异常去哪了?
结论先行:execute(Runnable) 没有返回值,任务抛出的异常会交给线程的 UncaughtExceptionHandler,线程随即终止并被替换;submit(...) 返回 Future,异常被 FutureTask 捕获存起来,不调 get() 就永远发现不了。这就是"线程池吞异常"的根源。
1. 逐项对比
| 维度 | execute(Runnable) | submit(Callable/Runnable) |
|---|---|---|
| 定义位置 | Executor 接口 | ExecutorService 接口 |
| 返回值 | void | Future<T> |
| 接受类型 | 仅 Runnable | Runnable 或 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 完就不管了"——那等于把线上错误静默丢进黑洞。