CompletionService
需求: 要做一个询价应用,这个应用需要从三个电商询价,然后保存在自己的数据库里
@PostMapping("/account/testSynFirst")
public void testSynFirst() throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(3);//创建线程池
Future<Integer> f1 = executor.submit(()->getPriceByS1()); //异步向S1询价
Future<Integer> f2 = executor.submit(()->getPriceByS2()); //异步向S2询价
Future<Integer> f3 = executor.submit(()->getPriceByS3()); //异步向S3询价
BlockingQueue<Integer> bq = new LinkedBlockingDeque<>(); //创建阻塞队列
executor.execute(()-> {
try {
bq.put(f1.get()); //电商S1报价异步进入阻塞队列
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
executor.execute(()-> { //电商S2报价异步进入阻塞队列
try {
bq.put(f2.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
executor.execute(()-> { //电商S3报价异步进入阻塞队列
try {
bq.put(f3.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
for (int i = 0; i < 3; i++) {
Integer r = bq.take();
executor.execute(()->save(r));
}
}
利用CompletionService实现询价系统
不过在实际项目中,并不建议你这样做,因为JAVA SDK并发包里已经提供了设计精良的CompletionService。利用CompletionService不但能帮你解决先获取到的报价先保存到数据库的问题,而且还能让代码更精炼
CompletionService的实现原来也是内部维护了一个阻塞队列,当任务执行结束就把任务的执行结果加入到阻塞队列中,不同的是CompletionService是把任务执行结果的Future对象加入到阻塞队列中,而上面的示例代码是把任务执行的最终结果放入到了阻塞队列中
CompletionService的使用
CompletionService接口的实现类是ExecutorCompletionService,这个实现类的构造方法有两个,分别是:
ExecutorCompletionService(Executor executor);
ExecutorCompletionService(Executor executor,BlockingQueue<Future<V>> completionQueue);
这两个构造方法都需要传入一个线程池,如果不指定completionQueue,那么默认会使用无界的LinkBlockingQueue,任务执行结果的Future对象就是加入到completionQueue中
下面的示例代码完整的展示了如何利用CompletionService来实现高性能的询价系统。其中,我们没有指定completionQueue,因此默认使用无界的LinkBlockingQueue
@PostMapping("/account/testSynSecond")
public void testSynSecond() throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(3);//创建线程池
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor); //创建CompletionService
cs.submit(()->getPriceByS1());//异步向电商S1询价
cs.submit(()->getPriceByS2());//异步向电商S2询价
cs.submit(()->getPriceByS3());//异步向电商S3询价
for (int i = 0; i < 3; i++) {
Integer r = cs.take().get();
executor.execute(()->save(r));
}
}
CompletionService接口说明
CompletionService接口提供的方法有5个,这5个方法的方法签名如下所示:
其中,submit()相关的方法有两个。一个方法参数是Callable task,另一个方法有两个参数,分别是Runnable task和 V result,这个方法类似于ThreadPoolExecutor的 Future submit(Runnable task, V result)。
CompletionService接口其他的3个方法,都是和阻塞队列相关的,take(),poll()都是从阻塞队列中获取并移除一个元素;它们的区别在于如果阻塞队列是空的,那么调用take()方法的线程会被阻塞,而poll()会返回null值。poll(long timeout, TimeUnit unit)方法支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了TimeUnit unit时间,阻塞队列还是空的,那么该方法会返回null值。
Future<V> submit(Callable<V> task);
Future<V> submit(Runnable task, V result);
Future<V> take() throws InterruptedException;
Future<V> poll();
Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
利用CompletionService实现Dubbo中的Forking Cluster
Dubbo中有一种叫着Forking的集群模式,这种集群模式下,支持并行的调用多个查询服务,只要有一个成功返回结果,整个服务就可以返回了。
@PostMapping("/account/testSynThird")
public Integer testSynThird() throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(3);
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);//创建CompletionService
List<Future<Integer>> futures = new ArrayList<>(3);//用于保存Future对象
//提交异步任务,并保存future到futures
futures.add(cs.submit(()->getPriceByS1()));
futures.add(cs.submit(()->getPriceByS2()));
futures.add(cs.submit(()->getPriceByS3()));
//获取最快返回的任务执行结果
Integer r = 0;
try {
for (int i = 0; i < 3; i++) {
r = cs.take().get();
//简单的通过判空来检查是否成功返回
if (r != null) {
break;
}
}
} finally {
//取消所有任务
for (Future<Integer> f : futures) {
f.cancel(true);
}
}
System.out.println(r);
return r;
}
总结
当需要批量提交异步任务的时候建议你使用CompletionService。CompletionService将线程池Executor和阻塞队列BlockingQueue的功能联合到了一起,能够让批量异步任务的管理更简单。除此之外,CompletionService能够让异步任务的执行结果有序化,先执行完成的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如Forking Cluster这样的需求
CompletionService的实现类ExecutorCompletionService,需要你自己创建线程池,随看上去有些啰嗦,但好处是你可以让多个ExecutorCompletionService的线程池隔离,这种隔离性能能避免几个特别耗时的任务拖垮整个应用的风险