Java线程池中execute()和submit()两个方法的区别(源码&实战全解析)
前言
在 Java 并发编程中,线程池是核心技术之一,而 execute() 和 submit() 是线程池最常用的两个方法。很多开发者只停留在表面认识——execute 抛异常,submit 返回 Future,但这种理解远远不够。
本文将从源码层面深度解析这两个方法的本质差异,并通过实战案例演示它们的适用场景。
一、核心差异一览
| 维度 | execute() | submit() |
|---|---|---|
| 返回值 | void | Future |
| 异常传播 | 任务内异常会直接抛出到 UncaughtExceptionHandler,主线程无法感知 | 异常被 FutureTask 捕获并存储,调用 get() 时才抛出 ExecutionException |
| 任务类型 | 仅支持 Runnable | 支持 Runnable 和 Callable |
| 适用场景 | 不关心结果的异步任务(如日志发送、数据清理) | 需要获取结果或处理异常的任务(如计算、RPC 调用) |
| 接口定义 | Executor 接口 | ExecutorService 接口 |
二、源码层面解析
2.1 submit() 的源码实现
// AbstractExecutorService.java
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
// 关键点1:将 Runnable 包装为 RunnableFuture
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask); // 关键点2:最终还是调用 execute()
return ftask; // 关键点3:返回 Future 对象
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
核心洞察:submit() 本质上是 execute() 的包装器,它在调用 execute() 前做了两件事:
- 任务包装:将 Runnable/Callable 包装成 FutureTask
- 返回句柄:给调用者一个 Future 对象用于获取结果
2.2 execute() 的核心逻辑
// ThreadPoolExecutor.java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1. workerCount < corePoolSize -> 创建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. workerCount >= corePoolSize && workQueue 未满 -> 入队
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3. workerCount >= corePoolSize && workQueue 已满 -> 创建非核心线程
else if (!addWorker(command, false))
// 4. 都失败 -> 拒绝策略
reject(command);
}
执行流程:
- 工作线程数 < 核心线程数 → 创建核心线程执行
- 工作线程数 ≥ 核心线程数,队列未满 → 任务入队
- 工作线程数 ≥ 核心线程数,队列已满 → 创建非核心线程
- 都失败 → 触发拒绝策略
三、实战场景对比
3.1 异常处理的根本差异
execute() 的异常陷阱:
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
throw new RuntimeException("任务异常");
});
// 主线程无法捕获这个异常!
// 异常会直接抛出到线程池的 UncaughtExceptionHandler
submit() 的异常安全:
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<?> future = executor.submit(() -> {
throw new RuntimeException("任务异常");
});
try {
future.get(); // 调用 get() 时才会抛出 ExecutionException
} catch (ExecutionException e) {
System.out.println("捕获到任务异常: " + e.getCause());
}
3.2 批量任务处理 - submit 优势场景
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Integer>> futures = new ArrayList<>();
// 提交多个任务
for (int i = 0; i < 10; i++) {
final int num = i;
futures.add(executor.submit(() -> compute(num)));
}
// 批量获取结果
for (Future<Integer> future : futures) {
try {
System.out.println(future.get());
} catch (Exception e) {
System.out.println("任务执行异常: " + e.getCause());
}
}
private int compute(int num) {
// 模拟计算任务
return num * num;
}
3.3 超时控制 - submit 独有能力
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
Thread.sleep(5000);
return "结果";
});
try {
String result = future.get(2, TimeUnit.SECONDS); // 2秒超时
System.out.println(result);
} catch (TimeoutException e) {
future.cancel(true); // 中断任务
System.out.println("任务超时,已取消");
}
3.4 execute 的最佳实践 - 异常监控
// 设置全局异常处理器
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.out.println("线程 " + t.getName() + " 发生异常: " + e);
});
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
throw new RuntimeException("异常会被 UncaughtExceptionHandler 捕获");
});
四、性能考量
- execute():略轻量,直接提交任务,无需创建 FutureTask 对象
- submit():因创建 FutureTask 有极小开销,但在实际业务中差异可忽略
- 建议:如果不需要返回值和异常处理,优先使用 execute()
五、面试标准答案
问题: Java 线程池中 execute() 和 submit() 有什么区别?
标准回答:
核心差异:execute() 是 Executor 接口定义的基础方法,用于提交不需要返回值的任务;submit() 是 ExecutorService 扩展的方法,可以提交 Callable/Runnable 并返回 Future 对象。
源码层面:submit() 内部将任务包装成 FutureTask,然后调用 execute() 执行,所以 execute() 是 submit() 的底层实现。
异常处理:这是最重要的区别——execute() 中任务的异常会直接抛出到线程池的异常处理器,主线程无法感知;submit() 中任务的异常被 FutureTask 捕获存储,只有调用 Future.get() 时才会抛出 ExecutionException,主线程可以统一处理。
适用场景:execute() 适合"提交即忘"的异步任务(如日志、清理);submit() 适合需要结果、超时控制或细粒度异常处理的任务。
性能考量:execute() 略轻量,submit() 因为创建 FutureTask 有极小开销,但在实际业务中差异可忽略。
六、进阶思考
6.1 为什么 submit() 要返回 Future?
这是"控制权"的设计哲学,调用者可以通过 Future 实现取消、超时、结果获取等精细控制。
6.2 线程池的拒绝策略对两者有区别吗?
没有,最终都是调用 execute(),拒绝策略统一生效。
6.3 如何既用 execute() 的轻量,又实现异常监控?
可以自定义 ThreadPoolExecutor,重写 afterExecute() 方法:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
System.out.println("任务执行异常: " + t);
} else if (r instanceof Future<?>) {
try {
((Future<?>) r).get();
} catch (Exception e) {
System.out.println("Future 异常: " + e.getCause());
}
}
}
};
七、总结
这道题的深层考点是:是否理解 Java 并发框架中"任务"和"执行"的分离设计,以及异常在不同线程上下文中的传播机制。
- execute():轻量级异步执行,适合"提交即忘"场景
- submit():功能完善,支持结果获取、超时控制、异常统一处理
选择建议:
- 不需要返回值 → 优先 execute()
- 需要返回值或异常处理 → 必须使用 submit()
到此这篇关于Java线程池中execute()和submit()两个方法区别的文章就介绍到这了,更多相关Java线程池execute()和submit()区别内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
java中超过long范围的超大整数相加算法详解(面试高频)
这篇文章主要介绍了java中超过long范围的超大整数相加算法(面试高频),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2020-08-08
SpringMVC项目访问controller时候报404的解决
这篇文章主要介绍了SpringMVC项目访问controller时候报404的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-09-09
SpringBoot集成Swagger2生成接口文档的方法示例
我们提供Restful接口的时候,API文档是尤为的重要,它承载着对接口的定义,描述等,本文主要介绍了SpringBoot集成Swagger2生成接口文档的方法示例,需要的朋友们下面随着小编来一起学习学习吧2018-12-12


最新评论