异步(并发)编程CompletionService
(学习笔记2020-08-24)
前言:
CompletionService
的主要应用场景是批量线程任务!学习之前,思考为什么要使用
CompletionService
, 不使用的结果是什么? 使用后的结果会是怎么样?
下面先来看1.0没有使用, 批量执行获取结果的线程池。
1.0 使用ExecutorService
批量执行任务获取结果
ExecutorService
批量提交任务有2种方式:一是自己将执行结果返回的
Future<T>
封装进集合。二是
ExecutorService
提供了invokeAll(Collection<? extends Callable<T>> tasks)
方法进行批量执行并返回List<Future<T>>
@Test
public void test() throws Exception {
List<Callable<Integer>> callableList = new ArrayList<>();
callableList.add((Callable<Integer>) () -> {
TimeUnit.SECONDS.sleep(8);
System.out.println("执行耗时用了8秒");
return 8;
});
callableList.add((Callable<Integer>) () -> {
TimeUnit.SECONDS.sleep(5);
System.out.println("执行耗时用了5秒");
return 5;
});
callableList.add((Callable<Integer>) () -> {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行耗时用了3秒");
return 3;
});
List<Future<Integer>> futureList = pools.invokeAll(callableList);
for (Future<Integer> future : futureList) {
Integer integer = future.get();
System.out.println(integer);
}
TimeUnit.SECONDS.sleep(30);
}
执行结果: (返回值是不是 3 5 8 还是 8 53是否和你预测的一样呢? )
执行耗时用了3秒
执行耗时用了5秒
执行耗时用了8秒
8
5
3从结果上看,明明就是3秒的结果执行最快,返回的最早, 但是调用
get()
获取结果的时候会阻塞其他执行快的结果, 先获取执行最慢的结果进行阻塞。 (假设一批任务, 第一个执行的任务是耗时最长的, 其他耗时短的执行完毕了, 你调用获取结果还是要进行等待获取最长耗时的线程结果才能进行处理, 白白浪费了其他耗时线程短的可优先执行完毕,可优先获取结果进行处理)
1.1 在看看我们之前学习的CompletableFuture
的批量执行结果
@Test
public void test() throws Exception {
List<CompletableFuture<Object>> callableList = new ArrayList<>();
callableList.add(CompletableFuture.supplyAsync(new Supplier<Object>() {
@SneakyThrows
@Override
public Object get() {
TimeUnit.SECONDS.sleep(8);
System.out.println("执行耗时用了8秒");
return 8;
}
}, pools));
callableList.add(CompletableFuture.supplyAsync(new Supplier<Object>() {
@SneakyThrows
@Override
public Object get() {
TimeUnit.SECONDS.sleep(5);
System.out.println("执行耗时用了5秒");
return 5;
}
}, pools));
callableList.add(CompletableFuture.supplyAsync(new Supplier<Object>() {
@SneakyThrows
@Override
public Object get() {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行耗时用了3秒");
return 3;
}
}, pools));
for (CompletableFuture<Object> future : callableList) {
//future.get() 调用get一样会阻塞结果按执行顺序获取
future.thenAccept((i)->{ //进行开启线程消费结果
System.out.println(Thread.currentThread().getName());
System.out.println((Integer) i);
});
}
TimeUnit.SECONDS.sleep(30);
}
执行结果:
执行耗时用了3秒
pool-1-thread-3
3
执行耗时用了5秒
pool-1-thread-2
5
执行耗时用了8秒
pool-1-thread-1
8
到此如果使用
ExecutorService
批量执行需要获取结果的任务, 弊端在与获取结果时候, 优先执行完毕的线程无法先获取到结果, 会获取第一个执行的线程结果, 如果第一个线程刚刚好是耗时最长的线程则会进行阻塞直到耗时最长的线程执行完毕获取到结果才阻塞结束获取到其他耗时短的。虽然
CompletableFuture
可以进行异步消费, 但是需要配置好足够的线程池。
2.0 CompletionService
批量执行任务获取结果
CompletionService
介绍: (好像是jdk1.8
提供的)将生产新的异步任务与完成任务的结果分离开来的服务。 生产者submit任务的执行。 消费者take完成的任务和处理它们的顺序结果,他们完成了。 CompletionService可以例如用于管理异步I / O,其中执行读取的程序的不同部分地基于在一个程序或系统的一个组成部分被提交,然后作用在所述读取完整,可能当任务不同的顺序比他们要求的。
通常, CompletionService依赖于一个单独的Executor来实际执行的任务,在这种情况下CompletionService只管理一个内部完成队列。 该ExecutorCompletionService类提供了此方法的实现。
内存一致性效果:提交任务的前行动线程CompletionService 发生,之前通过该任务,这反过来又发生,之前的动作之后,从相应的成功返回所采取的行动take()翻译来的, 总结就一句话: 谁先完成就可以调用
take()
获取先完成的结果。直接来看看同样的例子批量执行结果吧!
2.1 创建方式与方法介绍
其
CompletionService
是个接口, 我们需要创建的是ExecutorCompletionService
实现类
ExecutorCompletionService
有2个构造方法:
ExecutorCompletionService(Executor executor)
ExecutorCompletionService(Executor executor,BlockingQueue<Future<V>> completionQueue)
都必须指定一个线程池, 第二个方法还可以指定一个阻塞队列。
CompletionService
原理就是执行完毕的任务结果会放到阻塞队列里面!感兴趣的可以去看看其他博客原理分析(点击打开)
提交任务执行有2个方法
Future<V> submit(Callable<V> task)
、Future<V> submit(Runnable task, V result)
获取结果的:
Future<V> take() throws InterruptedException;
检索并移除表示下一个已完成任务,等待如果目前不存在这样的未来 (本质就是调用队列的take()
方法,获取并移除头部)
Future<V> poll()
与Future<V> poll(long timeout, TimeUnit unit)
获取并移除表示下一个已完成任务,或者未来null ,如果不存在 (本质就是调用队列的poll()
方法,获取不到返回null并移除头部)
2.2 开始使用
@Test
public void test() throws Exception {
List<Future<?>> futureList = new ArrayList<>();
CompletionService completionService = new ExecutorCompletionService(pools);
futureList.add(completionService.submit(() -> {
TimeUnit.SECONDS.sleep(8);
System.out.println("执行耗时用了8秒");
return 8;
}));
futureList.add(completionService.submit(() -> {
TimeUnit.SECONDS.sleep(5);
System.out.println("执行耗时用了5秒");
return 5;
}));
futureList.add(completionService.submit(() -> {
TimeUnit.SECONDS.sleep(3);
System.out.println("执行耗时用了3秒");
return 3;
}));
//批量执行多少线程就循环获取多少次, 调用take()方法从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值
for (int i = 0; i < futureList.size(); i++) {
Future take = completionService.take();
System.out.println(take.get());
}
//让gc回收
futureList = null;
TimeUnit.SECONDS.sleep(20);
}
执行结果:
执行耗时用了3秒
3
执行耗时用了5秒
5
执行耗时用了8秒
8
到此完毕, 批量执行线程任务, 先执行完毕的先获取结果进行后续操作使用
CompletionService
1