多线程submit()和execute()的区别
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
任务中抛出未捕获的异常,异常会直接传播到线程池的未捕获异常处理器(默认打印堆栈,但程序不会停止),但线程会因异常退出,这可能导致线程池中的线程减少,进而影响后续任务的执行。所以,在选择使用哪个方法时,需要根据具体的业务需求来决定。