并发工具类_04

71 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

25 | CompletionService:如何批量执行异步任务?

如何优化一个询价应用的核心代码?

如果采用“ThreadPoolExecutor+Future”的方案,用三个线程异步执行询价,通过三次调用 Future 的 get() 方法获取询价结果,之后将询价结果保存在数据库中。

// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);

// 异步向电商S1询价
Future<Integer> f1 = executor.submit(()->getPriceByS1());
// 异步向电商S2询价
Future<Integer> f2 = executor.submit(()->getPriceByS2());
// 异步向电商S3询价
Future<Integer> f3 = executor.submit(()->getPriceByS3());
    
// 获取电商S1报价并保存
r=f1.get();
executor.execute(()->save(r));
  
// 获取电商S2报价并保存
r=f2.get();
executor.execute(()->save(r));
  
// 获取电商S3报价并保存  
r=f3.get();
executor.execute(()->save(r));

如何利用阻塞队列实现先获取到的报价先保存到数据库。

// 创建阻塞队列
BlockingQueue<Integer> bq = new LinkedBlockingQueue<>();

//电商S1报价异步进入阻塞队列  
executor.execute(()-> bq.put(f1.get()));
//电商S2报价异步进入阻塞队列  
executor.execute(()-> bq.put(f2.get()));
//电商S3报价异步进入阻塞队列  
executor.execute(()-> bq.put(f3.get()));

//异步保存所有报价  
for (int i=0; i<3; i++) {
    Integer r = bq.take();
    executor.execute(()->save(r));
}  

利用 CompletionService 实现询价系统

Java SDK 并发包里已经提供了设计精良的 CompletionService。利用 CompletionService 不但能帮你解决先获取到的报价先保存到数据库的问题,而且还能让代码更简练。

// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);

// 创建CompletionService
CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);

// 异步向电商S1询价
cs.submit(()->getPriceByS1());
// 异步向电商S2询价
cs.submit(()->getPriceByS2());
// 异步向电商S3询价
cs.submit(()->getPriceByS3());

// 将询价结果异步保存到数据库
for (int i=0; i<3; i++) {
    Integer r = cs.take().get();
    executor.execute(()->save(r));
}

CompletionService 接口提供的方法有 5 个。

其中,submit() 相关的方法有两个。

CompletionService 接口其余的 3 个方法,都是和阻塞队列相关的,take()poll() 都是从阻塞队列中获取并移除一个元素;它们的区别在于如果阻塞队列是空的,那么调用 take() 方法的线程会被阻塞,而 poll() 方法会返回 null 值。

poll(long timeout, TimeUnit unit) 方法支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout 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。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue 的功能融合在了一起,能够让批量异步任务的管理更简单。

除此之外,CompletionService 能够让异步任务的执行结果有序化,先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如 Forking Cluster 这样的需求。

CompletionService 的实现类 ExecutorCompletionService,需要你自己创建线程池,虽看上去有些啰嗦,但好处是你可以让多个 ExecutorCompletionService 的线程池隔离,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险。

26 | Fork/Join:单机版的MapReduce

对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;

如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;

而批量的并行任务,则可以通过 CompletionService 来解决。

从上到下,依次为简单并行任务、聚合任务和批量并行任务示意图

image.png

ForkJoinPool 工作原理

Fork/Join 并行计算的核心组件是 ForkJoinPool,下面介绍其工作原理。

image.png