多线程submit()和execute()的区别

后端 潘老师 2周前 (04-10) 12 ℃ (0) 扫码查看

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


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/17146.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】