Java多线程编程submit()execute()是两个常用的向线程池提交任务的方法。这两个方法虽然功能相近,但在很多方面存在差异。下面咱们就详细分析一下它们的不同之处。

一、方法定义与功能

execute()方法定义在Executor接口中,它只能提交Runnable类型的任务,并且没有返回值,这意味着我们没办法通过它获取任务的执行结果。要是任务执行过程中抛出异常,会由线程池的未捕获异常处理器来处理。来看一下它的源码:

public interface Executor { /** * 在未来某个时间执行给定的命令。该命令可能在新线程、线程池中的线程或调用线程中执行,具体取决于{@code Executor}的实现。 * * @param command 可运行的任务 * @throws RejectedExecutionException 如果此任务无法被接受执行 * @throws NullPointerException 如果command为null */ void execute(Runnable command); } 

submit()方法则定义在ExecutorService接口中,这个接口继承自Executor接口。submit()方法功能更强大一些,它既可以提交Runnable任务,也能提交Callable任务,并且会返回一个Future对象。通过这个Future对象,我们能获取任务的执行结果,还能取消任务。对于Runnable任务,任务完成后返回的Future对象会是null,而任务执行时的异常会被捕获并存储在Future中,只有调用Future.get()方法时才会抛出异常。下面是它的源码:

// ExecutorService继承Executor public interface ExecutorService extends Executor { /** * 提交一个有返回值的任务用于执行,并返回一个表示任务待完成结果的Future。Future的{@code get}方法将在任务成功完成时返回任务的结果。 * * <p> * 如果你想立即阻塞等待任务完成,可以使用{@code result = exec.submit(aCallable).get();}这种形式的代码。 * * <p>注意:{@link Executors}类包含一组方法,可以将其他一些常见的类似闭包的对象,例如{@link java.security.PrivilegedAction}转换为{@link Callable}形式,以便提交。 * * @param task 要提交的任务 * @param <T> 任务结果的类型 * @return 一个表示任务待完成的Future * @throws RejectedExecutionException 如果任务无法被调度执行 * @throws NullPointerException 如果任务为null */ <T> Future<T> submit(Callable<T> task); /** * 提交一个Runnable任务用于执行,并返回一个表示该任务的Future。Future的{@code get}方法将在任务成功完成时返回给定的结果。 * * @param task 要提交的任务 * @param result 要返回的结果 * @param <T> 结果的类型 * @return 一个表示任务待完成的Future * @throws RejectedExecutionException 如果任务无法被调度执行 * @throws NullPointerException 如果任务为null */ <T> Future<T> submit(Runnable task, T result); /** * 提交一个Runnable任务用于执行,并返回一个表示该任务的Future。Future的{@code get}方法将在任务成功完成时返回{@code null}。 * * @param task 要提交的任务 * @return 一个表示任务待完成的Future * @throws RejectedExecutionException 如果任务无法被调度执行 * @throws NullPointerException 如果任务为null */ Future<?> submit(Runnable task); } 

submit()方法有三种重载形式,下面分别介绍一下:

提交Callable任务获取执行结果

<T> Future<T> submit(Callable<T> task); 

使用这个方法提交Callable任务时,它会返回该线程的执行结果。示例代码如下:

Future<String> future = threadPool.submit(() -> { Thread.sleep(1000); return "Hello from Callable!"; }); System.out.println(future.get()); // 输出 "Hello from Callable!" 

在这段代码里,submit()方法提交了一个Callable任务,任务执行完成后,通过future.get()就能获取到任务返回的字符串结果。

提交Runnable任务并指定返回结果

Future<T> submit(Runnable task, T result); 

这个方法用于提交Runnable任务,同时可以指定任务执行后的返回结果。我们知道Runnable本身是没有返回值的,但有时候我们希望有个标识来判断程序是否执行完毕,就可以用这个方法指定返回值。示例如下:

Future<String> future = threadPool.submit( () -> { try { System.out.println("Processing data..."); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }, "SUCCESS" // 任务完成后返回这个字符串 ); System.out.println(future.get()); // 输出 "SUCCESS"(而不是 null) 

假设存在一个不需要返回计算结果的任务(Runnable类型),但我们又希望Future能返回一个状态信息(比如"SUCCESS""FAILED"),而不是null,这时就可以使用这个方法。

提交Runnable任务不获取返回值

Future<?> submit(Runnable task); 

这种形式提交的Runnable任务没有返回值。示例代码如下:

Future<?> future = threadPool.submit(() -> { System.out.println("Running a Runnable task"); }); future.get(); // 返回 null 

二、异常处理的差异

execute()的异常处理

当使用execute()提交任务并执行线程时,如果任务中抛出异常且没有被捕获,异常会直接打印在控制台。例如下面这段代码:

public class ExecuteUncaughtException { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { System.out.println("任务开始执行..."); throw new RuntimeException("这是一个未捕获的异常!"); // 未捕获的异常 }); // 后续任务可能不会执行(因为线程可能已终止) threadPool.execute(() -> System.out.println("后续任务")); threadPool.shutdown(); } } 

输出结果为:

任务开始执行... Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 这是一个未捕获的异常! at ExecuteUncaughtException.lambda$main$0(ExecuteUncaughtException.java:10) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) 

从这个例子可以看出,execute()处理异常存在一些缺点:异常如果未被捕获,会直接抛出并打印到控制台;而且线程池中的线程可能因为异常而终止,特别是在newSingleThreadExecutor这种只有一个线程的情况下,会导致后续任务无法执行。

submit()的异常处理

使用submit()提交任务并执行线程时,发生的异常会被存储在FutureTask中,只有当调用ft.get()时,这个异常才会被抛出。示例代码如下:

import java.util.concurrent.*; public class SubmitUncaughtException { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); Future<?> future = threadPool.submit(() -> { System.out.println("任务开始执行..."); throw new RuntimeException("这是一个未捕获的异常!"); }); try { //使用future.get,就在编译时必须处理异常 future.get(); // 在这里抛出 ExecutionException } catch (ExecutionException e) { System.err.println("捕获到 Future 中的异常: " + e.getCause()); // 获取原始异常 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 后续任务正常执行 threadPool.execute(() -> System.out.println("后续任务")); threadPool.shutdown(); } } 

输出结果为:

任务开始执行... 捕获到 Future 中的异常: java.lang.RuntimeException: 这是一个未捕获的异常! 后续任务 

从这里可以看到,要想让异常不影响线程的执行,需要使用try-catch来处理异常。

如果在execute()中也捕获异常,情况会怎样呢?来看下面的代码:

ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { try { System.out.println("任务开始执行..."); throw new RuntimeException("这是一个捕获的异常!"); } catch (RuntimeException e) { System.err.println("捕获到异常: " + e.getMessage()); // 捕获并处理 } }); // 后续任务正常执行 threadPool.execute(() -> System.out.println("后续任务")); threadPool.shutdown(); 

输出结果为:

任务开始执行... 捕获到异常: 这是一个捕获的异常! 后续任务 

从结果可以看出,当在execute()中捕获并处理异常时,和submit()的执行过程是一样的。

那么为什么很多人说submit()处理异常更强大呢?主要有两个原因:
第一,使用submit()执行线程时,通过future.get()获取执行结果需要显式地使用try-catch处理异常(因为future.get()会抛出受检异常,不使用try-catch在编译时就会报错)。不过,对于有良好编程习惯且熟练使用execute()的人来说,在execute()的线程中使用try-catch也能达到类似效果,所以这一点优势并不明显。
第二,也是submit()真正强大的地方,execute()的异常信息无法向上传递,当execute()的异常被消化后,调用方完全不知道任务失败了(除非手动记录日志)。而submit()的异常可以通过Future.get()传递给调用方,这种方式更适合需要统一错误处理的场景。

三、总结

在实际开发中,我们经常会用到ExecutorService接口,因为它继承了Executor接口,所以既可以使用execute()方法,也能使用submit()方法。不过综合来看,submit()方法更强大一些,它支持返回值(即使是Runnable任务也能有预期的返回值),并且在异常处理方面表现更好。而execute()如果在Runnable任务中抛出未捕获的异常,异常会直接传播到线程池的未捕获异常处理器(默认打印堆栈,但程序不会停止),但线程会因异常退出,这可能导致线程池中的线程减少,进而影响后续任务的执行。所以,在选择使用哪个方法时,需要根据具体的业务需求来决定。