高效并发任务调度:ExecutorCompletionService的策略与技巧

本文主要介绍ExecutorCompletionService的使用技巧及在实际项目中的运用

基本介绍

CompletionService 内部维护了一个阻塞队列,当任务执行结束就把任务执行结果的 Future 对象加入到阻塞队列中。

构造方法

CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个

ExecutorCompletionService(Executor executor)
ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)

这两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。

接口说明

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;

官方使用说明

查看jdk源码,可以看到主要有两个使用场景

场景一:并发执行任务并处理结果

假设我们有一个任务列表,每个任务返回一个字符串结果,我们想要并发执行这些任务,并打印出所有非空的结果。

static void demo1() {
       // 创建固定大小的线程池(实际使用中不建议这样做)
       ExecutorService executor = Executors.newFixedThreadPool(4);
       CompletionService<String> completionService = new ExecutorCompletionService<>(executor);

       // 模拟任务列表
       List<Callable<String>> tasks = Arrays.asList(
               () -> "result 1",

               () -> "result 2",

               () -> "result 3",

               () -> null
       );

       try {
           // 提交所有任务
           for (Callable<String> task : tasks) {
               completionService.submit(task);
           }

           // 处理结果
           for (int i = 0; i < tasks.size(); i++) {
               Future<String> future = completionService.take(); // 阻塞等待任务完成
               String result = future.get(); // 获取结果
               if (result != null) {
                   System.out.println("processed: " + result);
               }
           }
       } catch (InterruptedException | ExecutionException e) {
           e.printStackTrace();
       } finally {
           executor.shutdown();
       }
}
场景二:使用第一个非空结果并取消其他任务

假设我们有多个任务尝试解决同一个问题,我们只需要第一个成功的结果,一旦得到,就取消其他的任务。

static void demo2() {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        CompletionService<String> completionService = new ExecutorCompletionService<>(executor);
        List<Future<String>> futures = new ArrayList<>();

        try {
            // 提交任务并存储Future
            for (int i = 0; i < 5; i++) {
                final int index = i;
                Callable<String> task = () -> {
                    // 模拟任务执行时间
                    Thread.sleep((long) (Math.random() * 100));
                    return "result " + (index + 1);
                };
                futures.add(completionService.submit(task));
            }

            String firstResult = null;
            while (firstResult == null && !futures.isEmpty()) {
                Future<String> future = completionService.take();
                String result = future.get();
                if (result != null) {
                    firstResult = result;
                    System.out.println("First non-null result: " + firstResult);
                    break; // 找到第一个非空结果,退出循环
                }
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 取消所有未完成的任务
            for (Future<String> future : futures) {
                if (!future.isDone()) {
                    future.cancel(true);
                }
            }
            executor.shutdown();
        }
    }

dubbo中的Forking Cluster

Dubbo 中有一种叫做 Forking 的集群模式,这种集群模式下,支持并行地调用多个查询服务,只要有一个成功返回结果,整个服务就可以返回了。

这即是上面场景二的使用场景

项目使用

在这篇文章,高效分页策略:掌握 LIMIT 语句的正确使用方法与最佳实践
其实是用到了,当时没有写完整,现在再来分析下是怎么使用的。

@Service
public class OrderInitServiceImpl{

    @Autowired
    private OdsPackOrderDAO odsPackOrderDAO;

    @Autowired
    private BizCommonService  bizCommonService;


    public void orderInit() throws InterruptedException {
        while(true){

            String packOrderCalculationSwitch = redisCluster.get("pack_order_switch");
            if(packOrderCalculationSwitch != null && packOrderCalculationSwitch.equals("switch_off")){
                break; //查询结束
            }
            
            List<OdsPackOrderDO> odsPackOrderDOList = bizCommonService.grabBizDataSlice(3,
                TimeUnit.MINUTES, 2000, odsPackOrderDAO, null);

            // 对查询出来的odsPackOrderDOList做一些业务逻辑
        }
    }

}

对查询出来的odsPackOrderDOList做一些业务逻辑,那么具体是怎么做的呢?

if(CollectionUtils.isNotEmpty(odsPackOrderDOList)){         
    int odsPackOrderSize = odsPackOrderDOList.size();
    //异步生产数据
    odsPackOrderDOList
        .stream()
        .forEach(odsPackOrderDO -> producerExecutor.submit(() -> {
            return orderService.buildOrderInitParam(odsPackOrderDO);
        }));

    //阻塞优先消费
    while (odsPackOrderSize > 0) {
        try {
            List<OrderInitParam> orderInitParamList;
            Future<List<OrderInitParam>> resultFuture = producerExecutor.take();
            if(resultFuture == null || (orderInitParamList = resultFuture.get()) == null){
                continue;
            }
            consumerExecutor.submit(() -> {
                orderService.orderInitExecute(orderInitParamList);
            });
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }finally {
            odsPackOrderSize --;
        }
    }
}

可以看到用到了两个线程池producerExecutorconsumerExecutor

  • producerExecutor:就是ExecutorCompletionService类型,负责生产数据
  • consumerExecutor:消费数据

总结

合理的利用异步框架可以显著的提升系统性能,除了CompletionService外,还有个CompletableFuture接口,下次分享一下它在项目中是怎么使用的。

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值