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。
Callable 和 Runnable 的区别是:
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 再继续完善。
这一节先把 execute 和 submit 的区别讲清楚。
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、超时、取消、任务状态这些东西。