跳到正文
hello world

24. execute 和 submit 的区别

发布于阅读量 0

24. execute 和 submit 的区别

前面用 ThreadPoolExecutor 批量处理 PDF 时,我用的是:

executor.execute(() -> {
    processPdf(file);
});

execute() 的作用很直接:提交一个任务给线程池执行。

但是它有一个明显限制:我拿不到任务的返回结果

如果只是简单执行任务,比如打印日志、处理文件、不关心结果,那 execute() 没问题。

但 PDF 水印这种场景,后面大概率会需要知道:

哪个 PDF 成功了;
哪个 PDF 失败了;
失败原因是什么;
生成后的文件路径是什么。

这时候就要看另一个方法:submit()


execute:只管提交任务

先看 execute()

它接收的是一个 Runnable

executor.execute(() -> {
    System.out.println("处理 PDF");
});

Runnable 的特点是:

public interface Runnable {
    void run();
}

也就是说,它没有返回值。

所以 execute() 更像是:

我把任务交给线程池;
至于任务执行完以后返回什么,我不关心。

比如:

executor.execute(() -> {
    System.out.println(Thread.currentThread().getName() + " 开始处理 PDF");

    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return;
    }

    System.out.println(Thread.currentThread().getName() + " PDF 处理完成");
});

这种任务执行完就结束了。

外面的 main 线程并不能直接拿到它的结果。


submit:提交任务,并返回 Future

submit() 不一样。

它会返回一个 Future

比如:

Future<String> future = executor.submit(() -> {
    Thread.sleep(3000);
    return "PDF 处理成功";
});

这里提交进去的是一个有返回值的任务。

submit() 返回的:

Future<String>

可以理解成一个“结果凭证”。

任务可能还没执行完,但我先拿到了这个凭证。

后面可以通过:

String result = future.get();

拿到任务结果。

如果任务还没完成,get() 会等待。


写一个简单对比

新建类:

com.succos.threadpool.ExecuteSubmitDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ExecuteSubmitDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        executor.execute(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " execute 开始执行任务");

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }

            System.out.println(Thread.currentThread().getName()
                    + " execute 任务执行完成");
        });

        Future<String> future = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " submit 开始执行任务");

            Thread.sleep(3000);

            System.out.println(Thread.currentThread().getName()
                    + " submit 任务执行完成");

            return "submit 返回:PDF 处理成功";
        });

        try {
            System.out.println("main 准备获取 submit 结果");

            String result = future.get();

            System.out.println("main 拿到结果:" + result);

        } catch (Exception e) {
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

这段代码里有两个任务。

第一个用 execute() 提交,没有返回结果。

第二个用 submit() 提交,返回了一个 Future<String>


future.get() 是等待点

这行代码很重要:

String result = future.get();

它的意思是:

如果任务已经执行完,就直接拿结果;
如果任务还没执行完,当前线程就在这里等。

在这个例子里,执行 future.get() 的是 main 线程。

所以就是:

main 线程等待 submit 提交的任务执行完成。

等任务返回:

return "submit 返回:PDF 处理成功";

main 才能拿到这个字符串。

所以 Future 很适合这种场景:

任务交给线程池执行;
我后面还想拿任务结果。

execute 和 submit 最直接的区别

可以先简单记成这样:

execute:提交任务,没有返回值;
submit:提交任务,有 Future 返回。

表面上看就是有没有返回值。

但实际使用时,它会影响后面的代码结构。

如果我用 execute(),任务成功失败一般只能在任务内部自己打印日志、自己处理异常。

如果我用 submit(),就可以把每个任务的结果收集起来,最后统一处理。

比如批量 PDF:

a.pdf -> 成功,输出路径 xxx;
b.pdf -> 失败,原因 xxx;
c.pdf -> 成功,输出路径 xxx。

这种结果收集,用 submit() 会更自然。


submit 可以提交 Callable

submit() 常见写法是提交 Callable

CallableRunnable 的区别是:

public interface Runnable {
    void run();
}
public interface Callable<V> {
    V call() throws Exception;
}

所以:

Runnable 没有返回值,也不能直接抛受检异常;
Callable 有返回值,而且可以抛异常。

这就是为什么下面这段代码能返回字符串:

Future<String> future = executor.submit(() -> {
    Thread.sleep(3000);
    return "PDF 处理成功";
});

这个 Lambda 对应的就是 Callable<String>

因为它有返回值。


submit 也可以提交 Runnable

submit() 也能提交 Runnable

比如:

Future<?> future = executor.submit(() -> {
    System.out.println("执行 Runnable 任务");
});

这种情况下,任务本身没有返回值。

future.get() 拿到的通常是 null

也可以这样写,给一个固定返回值:

Future<String> future = executor.submit(() -> {
    System.out.println("执行 Runnable 任务");
}, "任务完成");

不过我平时更常用的还是:

Future<String> future = executor.submit(() -> {
    return "任务结果";
});

因为既然用了 submit(),多数时候就是想要结果。


异常表现也不一样

execute()submit() 在异常表现上也有差异。

先看 execute()

如果任务里抛了运行时异常:

executor.execute(() -> {
    throw new RuntimeException("execute 任务异常");
});

异常通常会直接在线程里打印出来。

submit() 不太一样。

比如:

Future<String> future = executor.submit(() -> {
    throw new RuntimeException("submit 任务异常");
});

这行 submit() 本身通常不会马上抛异常。

异常会被封装到 Future 里。

等我调用:

future.get();

才会抛出 ExecutionException

可以写个例子看一下。


submit 异常示例

新建类:

com.succos.threadpool.SubmitExceptionDemo

代码如下:

package com.succos.threadpool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class SubmitExceptionDemo {

    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        Future<String> future = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()
                    + " 开始执行 submit 任务");

            int result = 1 / 0;

            return "处理成功:" + result;
        });

        try {
            System.out.println("main 准备获取结果");

            String result = future.get();

            System.out.println("结果:" + result);

        } catch (Exception e) {
            System.out.println("future.get() 获取结果失败");
            e.printStackTrace();
        }

        executor.shutdown();
    }
}

这里任务内部有一行:

int result = 1 / 0;

肯定会抛异常。

但异常不会在 submit() 那一行直接抛出来。

而是在:

future.get();

这里被拿出来。

所以如果用了 submit(),但从来不调用 future.get(),任务里的异常可能就没那么明显。

这点要注意。


批量 PDF 里为什么 submit 更适合收集结果

假设我现在想处理多个 PDF,并且最后输出每个文件的处理结果。

如果用 execute(),大概只能这样:

executor.execute(() -> {
    try {
        processPdf(file);
        System.out.println(file.getName() + " 成功");
    } catch (Exception e) {
        System.out.println(file.getName() + " 失败");
    }
});

每个任务自己打印自己的结果。

但如果我想最后统一汇总,就不太方便。

submit() 可以这样:

Future<String> future = executor.submit(() -> {
    processPdf(file);
    return file.getName() + " 处理成功";
});

然后把所有 Future 放到集合里:

List<Future<String>> futureList = new ArrayList<>();
futureList.add(future);

最后统一遍历:

for (Future<String> future : futureList) {
    String result = future.get();
    System.out.println(result);
}

这样就能把每个 PDF 的处理结果集中拿到。


一个批量 submit 示例

新建类:

com.succos.threadpool.ThreadPoolSubmitBatchDemo

代码如下:

package com.succos.threadpool;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolSubmitBatchDemo {

    public static void main(String[] args) {

        File inputDir = new File("input");

        File[] files = inputDir.listFiles(file ->
                file.isFile() && file.getName().toLowerCase().endsWith(".pdf")
        );

        if (files == null || files.length == 0) {
            System.out.println("input 目录下没有 PDF 文件");
            return;
        }

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3,
                3,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        List<Future<String>> futureList = new ArrayList<>();

        for (File file : files) {
            Future<String> future = executor.submit(() -> {
                System.out.println(Thread.currentThread().getName()
                        + " 开始处理:"
                        + file.getName());

                Thread.sleep(3000);

                System.out.println(Thread.currentThread().getName()
                        + " 处理完成:"
                        + file.getName());

                return "成功:" + file.getName();
            });

            futureList.add(future);
        }

        for (Future<String> future : futureList) {
            try {
                String result = future.get();
                System.out.println("任务结果:" + result);
            } catch (Exception e) {
                System.out.println("任务执行失败:" + e.getMessage());
            }
        }

        executor.shutdown();

        System.out.println("全部任务结果收集完成");
    }
}

这段代码的结构是:

先提交所有任务;
把每个 Future 保存起来;
再统一遍历 Future 获取结果。

这个顺序很重要。


不要提交一个就 get 一个

这里也有一个坑。

如果我这样写:

for (File file : files) {
    Future<String> future = executor.submit(() -> {
        processPdf(file);
        return "成功:" + file.getName();
    });

    String result = future.get();
    System.out.println(result);
}

这段代码能运行,但并发效果会变差。

因为每次提交一个任务以后,马上 get() 等它完成。

执行流程就变成:

提交 a.pdf;
main 等 a.pdf 处理完;

提交 b.pdf;
main 等 b.pdf 处理完;

提交 c.pdf;
main 等 c.pdf 处理完。

这就接近顺序执行了。

所以批量任务时,我一般会这样写:

先全部 submit;
再统一 get。

这个习惯和前面 Thread 里的:

先全部 start;
再统一 join。

其实是同一个思路。


PDF 水印任务返回什么比较合适

如果只是学习,可以返回字符串:

return "成功:" + file.getName();

但真实一点的写法,我更倾向于返回一个结果对象。

比如:

public class PdfTaskResult {

    private boolean success;

    private String fileName;

    private String targetPath;

    private String message;
}

成功时返回:

success = true
fileName = a.pdf
targetPath = output/a-watermark.pdf
message = 处理成功

失败时返回:

success = false
fileName = b.pdf
targetPath = null
message = 失败原因

这样后面统计成功数、失败数、失败原因会更方便。

不过这个结果对象可以放到下一节 Future 再继续完善。

这一节先把 executesubmit 的区别讲清楚。


execute 和 submit 怎么选

我现在的判断比较简单。

如果只是执行任务,不关心返回结果:

executor.execute(() -> {
    processPdf(file);
});

比如:

异步写日志;
发送不关心结果的通知;
简单后台任务。

可以用 execute()

如果要拿任务结果:

Future<String> future = executor.submit(() -> {
    processPdf(file);
    return "处理成功";
});

比如:

要知道 PDF 处理是否成功;
要拿到输出路径;
要统计失败原因;
要统一汇总任务结果。

就用 submit()


execute 和 submit 都不是等待全部任务

还有一点要分清楚。

无论是:

executor.execute(...)

还是:

executor.submit(...)

它们本身都只是提交任务。

提交完不代表任务执行完。

如果要等待任务完成,可以用:

Future.get():等待某一个任务完成并拿结果;
executor.awaitTermination():等待线程池里所有任务结束;
CompletableFuture.allOf().join():等待一批 CompletableFuture 结束。

这几个等待方式解决的问题不一样,后面还会继续区分。


这一节小结

这一节我主要记住几点:

1. execute 用来提交 Runnable,没有返回值;
2. submit 会返回 Future,可以通过 Future.get() 获取结果;
3. submit 常用来提交 Callable,因为 Callable 有返回值;
4. future.get() 是等待点;
5. submit 里的异常通常会在 future.get() 时暴露;
6. 批量任务不要提交一个就 get 一个,要先全部提交,再统一获取结果;
7. PDF 批量处理如果要统计成功失败,更适合 submit + Future。

用一句话总结:

execute 适合“只执行”,submit 适合“执行完还要拿结果”。

下一节继续看 Future

因为 submit 返回的 Future 才是拿异步结果的关键,里面还涉及 get、超时、取消、任务状态这些东西。

24. execute 和 submit 的区别 - Java并发之从Thread到CompletableFuture - 上下文网