在Java并发编程中,ThreadPoolExecutor 是我们最常打交道的线程池实现类。它提供了几个核心方法来管理任务的提交与线程池的生命周期。很多开发者虽然每天都在使用 execute、submit 和 shutdown,但对它们的内部实现原理、区别以及隐藏的陷阱却了解不深。本文将深入剖析这三个核心方法,从源码层面解读它们的执行逻辑,帮助你写出更健壮的并发代码。
一、execute(Runnable command):最基础的任务提交
execute 是 Executor 接口中定义的方法,也是线程池执行任务的入口 。它接收一个 Runnable 任务,没有返回值,且无法直接感知任务执行结果或异常。
1. 执行流程回顾
在调用 execute 时,线程池会按照以下顺序处理:
-
线程数 < corePoolSize → 创建新核心线程执行任务。
-
线程数 ≥ corePoolSize → 尝试入队。
-
入队失败(队列已满)且线程数 < maximumPoolSize → 创建非核心线程执行任务。
-
入队失败且线程数 = maximumPoolSize → 触发拒绝策略。
2. 源码深度分析
java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 步骤1:如果工作线程数小于核心线程数,尝试添加核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 步骤2:线程池处于RUNNING状态,尝试将任务加入队列
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:尝试创建非核心线程,若失败则执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
关键点解析:
-
addWorker(Runnable firstTask, boolean core):这是真正创建线程的方法。core参数决定比较的是corePoolSize还是maximumPoolSize。如果创建成功,新线程会立即执行firstTask(若不为空)。 -
双重检查:在入队成功后,会再次检查线程池状态。如果此时线程池已被关闭,则需将任务从队列中移除并拒绝;如果状态正常但工作线程数为0(可能核心线程全被回收),则创建一个非核心线程来执行队列中的任务。
-
workQueue.offer(command):入队操作是非阻塞的,如果队列已满会立即返回false,从而进入步骤3。
3. 异常处理
通过 execute 提交的任务,如果在执行过程中抛出未捕获的异常,该异常会直接打印到控制台(或由 Thread 的 UncaughtExceptionHandler 处理),并且执行该任务的线程会终止 (线程池会创建新线程替换)。因此,对于 execute 提交的任务,建议在任务内部做好异常捕获。
二、submit(Callable<T> / Runnable):带返回值的任务提交
submit 是 ExecutorService 接口中定义的方法,它是对 execute 的封装,允许提交有返回值的任务(Callable)或带返回结果的 Runnable。submit 返回一个 Future 对象,通过它我们可以获取任务执行结果、异常信息,甚至取消任务。
1. 内部实现
submit 方法在 AbstractExecutorService 中实现,最终调用的是 execute。让我们看看源码:
java
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
其中 newTaskFor 方法创建了一个 FutureTask 对象,它同时实现了 Runnable 和 Future 接口。FutureTask 内部封装了任务的执行结果和状态,当 execute 执行时,实际执行的是 FutureTask 的 run 方法。任务执行完毕后,结果会保存在 FutureTask 中,供 Future.get() 获取。
2. 异常处理的关键区别
通过 submit 提交的任务,如果抛出异常,异常不会立即打印到控制台 ,而是被封装在 Future 中。当你调用 future.get() 时,会抛出 ExecutionException,其 cause 就是原始异常。
这种机制使得异常处理更加可控,不会因为一个任务异常导致线程意外终止(线程会继续复用)。但也需要注意:如果你从未调用 get(),异常会被"吞掉",难以发现。
3. 使用建议
-
如果不需要关心任务结果,可以使用
execute,但要处理好异常。 -
如果需要获取结果或感知异常,使用
submit,并确保调用get()(可以设置超时,避免无限等待)。 -
注意
Future.get()是阻塞的,可能造成线程阻塞。
三、shutdown() 与 shutdownNow():优雅与强硬的关闭
线程池在使用完毕后需要显式关闭,以释放系统资源。ExecutorService 提供了两种关闭方式:shutdown() 和 shutdownNow()。
1. shutdown():优雅关闭
java
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 检查安全权限
checkShutdownAccess();
// 将状态切换为 SHUTDOWN
advanceRunState(SHUTDOWN);
// 中断空闲线程(核心线程不会中断,因为它们可能正在等待任务)
interruptIdleWorkers();
// 钩子方法,供子类扩展
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
shutdown() 的行为:
-
停止接收新任务(调用
execute会触发拒绝策略)。 -
继续执行队列中已有的任务。
-
不会中断正在执行的任务。
-
当队列为空且所有任务执行完毕后,线程池最终进入
TERMINATED状态。
注意 :shutdown() 是异步的,它不会等待所有任务完成。如果需要等待,可以配合 awaitTermination 使用。
2. shutdownNow():强制关闭
java
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 将状态切换为 STOP
advanceRunState(STOP);
// 中断所有工作线程(包括正在执行的)
interruptWorkers();
// 取出未执行的任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
shutdownNow() 的行为:
-
停止接收新任务。
-
尝试中断所有正在执行的任务(通过
Thread.interrupt())。 -
不再处理队列中尚未执行的任务,并将它们返回给调用者。
-
如果任务不响应中断,线程仍可能继续执行完毕。
3. 优雅关闭的最佳实践
java
ExecutorService executor = Executors.newFixedThreadPool(10);
// ... 提交任务 ...
// 1. 停止接收新任务
executor.shutdown();
try {
// 2. 等待一段时间,让已有任务完成
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 超时后,强制关闭
executor.shutdownNow();
// 4. 再等一会,让中断生效
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未能终止");
}
}
} catch (InterruptedException e) {
// 5. 如果当前线程被中断,也尝试关闭线程池
executor.shutdownNow();
Thread.currentThread().interrupt();
}
4. 关于 awaitTermination
awaitTermination 会阻塞当前线程,直到以下条件之一发生:
-
所有任务完成,线程池终止。
-
超时时间到。
-
当前线程被中断。
它本身不会关闭线程池,必须配合 shutdown 或 shutdownNow 使用。
四、三个方法的使用场景对比
| 方法 | 返回值 | 异常处理 | 使用场景 |
|---|---|---|---|
execute |
无 | 异常直接抛出,可能导致线程终止 | 提交无需结果的任务,任务内部已处理好异常 |
submit |
Future |
异常封装在 Future 中,通过 get() 获取 |
需要获取结果或感知异常,任务可能抛受检异常 |
shutdown |
无 | --- | 优雅关闭,允许执行完已提交任务 |
shutdownNow |
未执行任务列表 | --- | 强制关闭,立即停止所有任务 |
五、常见误区与注意事项
1. shutdown() 后还能调用 submit() 吗?
不能。shutdown() 执行后,线程池状态变为 SHUTDOWN,再次调用 submit() 会触发拒绝策略(默认抛出异常)。
2. shutdownNow() 一定能中断线程吗?
不一定。shutdownNow() 只是调用了线程的 interrupt() 方法,如果任务代码没有正确处理 InterruptedException 或没有检查中断状态,线程可能继续运行。
3. 使用 submit 但不调用 get() 会怎样?
如果任务抛出异常,异常会被"吞掉",且不会打印任何日志。这可能导致问题难以排查。因此,即使不关心结果,也建议调用 get() 或通过 Future 的其他方式处理异常。
4. 忘记关闭线程池的后果
如果线程池一直未关闭,其核心线程会一直存活(即使空闲),这会导致应用程序无法正常退出,造成资源泄漏。
5. submit 和 execute 的性能差异
submit 相比 execute 多了一层 FutureTask 的包装,会有微小的性能开销。但在大多数业务场景下可以忽略不计,更应关注其带来的便利性。
六、源码视角下的线程池生命周期
了解线程池的状态转换有助于理解 shutdown 和 shutdownNow 的行为。线程池状态定义在 ThreadPoolExecutor 中:
-
RUNNING:接受新任务,处理队列任务。
-
SHUTDOWN:不接受新任务,处理队列任务。
-
STOP:不接受新任务,不处理队列任务,中断正在执行的任务。
-
TIDYING:所有任务已终止,工作线程数为0。
-
TERMINATED :
terminated()方法执行完成。
shutdown 将状态从 RUNNING 改为 SHUTDOWN,shutdownNow 改为 STOP。tryTerminate() 方法负责在合适的时候将状态推进到 TIDYING 和 TERMINATED。
七、总结
execute、submit 和 shutdown 是线程池使用的三个基石。execute 负责最基础的任务提交,submit 提供了更强大的结果获取和异常处理能力,而 shutdown 系列方法则保障了线程池的优雅退出。理解它们的内部原理,不仅可以帮助我们写出更安全的并发代码,还能在遇到问题时快速定位。
在实际开发中,建议:
-
对无需结果的任务,优先使用
execute并处理好异常。 -
对需要结果或异常感知的任务,使用
submit并务必处理Future。 -
始终记得在应用关闭时调用
shutdown并配合awaitTermination实现优雅退出。