• Java批处理ExecutorService/CompletionService


    服务端接收一个请求,常常需要同时进行几个计算或者向其他服务发送请求,最后拼装结果返回上游。本文就来看下JDK提供几个并行处理方案,牵涉到ExcecutorService/CompletionService。要实现的场景是请求有超时限制,如果所有操作都计算完成,则全部拼装返回;否则只拼装部分完成的结果。

    1.前提

    //任务类,sleep一个时间代表这个计算需要的耗时,返回一个计算结果。
    public class MyTask implements Callable<Integer> {
        private int id;
        private int time;
        public MyTask(int i, int time) {
            this.id = i;
            this.time = time;
        }
        @Override
        public Integer call() throws Exception {
            Thread.sleep(time);
            return id;
        }
    }
    //线程池
    ExecutorService threadPool = new ThreadPoolExecutor(10, 20, 
    60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));

    2.任务逐个等待

    提交任务后,逐个等待结果返回。

    //总共150ms超时
    List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
    futures.add(threadPool.submit(new MyTask(1, 60)));
    futures.add(threadPool.submit(new MyTask(2, 150)));
    Integer res = 0;
    
    for (int i = 0; i < futures.size(); ++i) {
    try {
    Integer tmp = futures.get(i).get(50 + i * 50, TimeUnit.MILLESECONDS);
    res += tmp;
    } catch (Exception e) {
    //nothing
    }
    }
    System.out.println(res);

    打印结果为0,实际上第60ms时第一个计算已经完成,本可以返回第一个结果的。这是因为生产者和消费者一一对应,调度不当。

    3.等待解耦

    用一个队列来实现生产者和消费者解耦,生产者把结果放到一个队列中,消费者从队列中取结果,不按照任务提交顺序等待,只要有结果就会消耗。CompletionService就是对Executor和异步队列的封装。

    CompletionService<Integer> service = new ExecutorCompletionService<Integer>(threadPool);
    List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
    futures.add(service.submit(new MyTask(1, 60)));
    futures.add(service.submit(new MyTask(2, 150)));
    
    List<Integer> result = new ArrayList<Integer>(futures.size());
    for (int i = 0; i < futures.size(); ++i) {
        Future<Integer> future = service.poll(50 + i * 50, TimeUnit.MILLISECONDS);
        if (null != future){
            result.add(future.get());
        }
    }
    int res = 0;
    for (Integer i : result) {
        res += null == i ? 0 : i;
    }
    System.out.println(res);

    打印结果为1,即第一个计算的结果。谁先完成就取谁。

    4.充分利用时间

    前面两个方案为每个任务都设置了一个固定的等待时间,两者之和不超过超时限制。这没有充分利用时间,两个任务是并发互不影响的,其各自可利用的时间应该是超时时间,而不应该是两者之和为超时时间。如果第一个任务耗时140,第二个耗时60,前两个任务就只能获得第二个任务的结果。如果两者都设置超时为150,就能获得2个结果。

    List<MyTask> tasks = new ArrayList<MyTask>();
    tasks.add(new MyTask(1, 140));
    tasks.add(new MyTask(2, 60));
    List<Future<Integer>> futures = threadPool.invokeAll(tasks, 150, TimeUnit.MILLISECONDS);
    
    int res = 0;
    for (Future<Integer> future : futures) {
        System.out.println("isDone:" + future.isDone());
        System.out.println("isCancel:" + future.isCancelled());
        if (future.isCancelled()) {
            continue;
        }
        res += future.get();
    }
    System.out.println(res);
    

    打印结果是3。上述方法利用ExecutorService的invokeAll方法,该方法在所有任务都完成或等待超时时返回,超时的时候会取消还没有完成的任务,通过判断任务是否被取消来判断任务是否计算完成。

    该文章说的是结果互不影响、有结果就能返回的应用场景。如果结果需要按照先后顺序进行合并,或者只需要等待一个计算完成,就不一定需要方案3了。

  • 相关阅读:
    洛谷 P1396 营救(二分答案)
    洛谷 P1807 最长路(toposort)
    洛谷 P1137 旅行计划(toposort)
    [bzoj3032][TYVJ P1924]七夕祭(环形均分纸牌,货仓选址)
    BZOJ3043 IncDec Sequence (差分)
    BZOJ2200 道路与航线(dijk+拓扑排序)
    P1462 通往奥格瑞玛的道路(二分+dijkstra)
    P1020 导弹拦截 O(nlogn)做法
    背包整理(01背包,完全背包,多重背包,分组背包)(待更新)
    [模板]树链剖分
  • 原文地址:https://www.cnblogs.com/whuqin/p/5035369.html
Copyright © 2020-2023  润新知