[java并发]java高并发系列- 第20天 JUC中的Executor框架详解2

原文链接:查看原文

感谢公众号“ 路人甲Java”的分享,如有冒犯,请联系删除,快去关注他吧
在这里插入图片描述

本文的主要内容

  1. ExecutorCompletionService出现的背景
  2. 介绍CompletionService接口及常用的方法
  3. 介绍ExecutorCompletionService类及其原理
  4. 示例一:执行一批任务,然后消费执行结果
  5. 示例二【2种方式】:异步执行一批任务,有一个完成立即返回,其他取消

需要解决的问题:

举例说明:

买新房了,然后在网上单买电冰箱、洗衣机,电器商家不同,所以送货耗时不一样,然后等他们送货,快递只愿送到楼下,然后我们自己将其搬到楼上的家中。

用程序来模拟上面的实现:

package aboutThread.Concurrent.Day20;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    static class GoodsModel {
        // 商品名称
        String name;
        // 购物开始时间
        long starttime;
        // 送到的时间
        long endtime;

        public GoodsModel(String name, long starttime, long endtime) {
            this.name = name;
            this.starttime = starttime;
            this.endtime = endtime;
        }

        @Override
        public String toString() {
            return name + ",下单时间[" + this.starttime + "," + this.endtime + "],耗时:" + (this.endtime - this.starttime);
        }

    }

    /**
     * 将商品搬上楼
     * 
     * @param goodsModel
     * @throws InterruptedException
     */
    static void moveUp(GoodsModel goodsModel) throws InterruptedException {
        // 休眠5秒
        TimeUnit.SECONDS.sleep(5);
        System.out.println("将商品搬上楼,商品信息:" + goodsModel);
    }

    /**
     * 
     * @param name
     * @param costTime
     * @return
     */
    static Callable<GoodsModel> buyGoods(String name, long costTime) {
        return () -> {
            long startTime = System.currentTimeMillis();
            System.out.println(startTime + "购买" + name + "下单!");
            // 模拟送货耗时
            try {
                TimeUnit.SECONDS.sleep(costTime);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            System.out.println(startTime + "," + name + "-送到了!");
            return new GoodsModel(name, startTime, endTime);
        };
    }

    public static void main(String[] args) throws InterruptedException,ExecutionException{
        long st = System.currentTimeMillis();
        System.out.println(st + ", 开始购物!");

        // 创建一个线程池,用来异步下单
        ExecutorService executor = Executors.newFixedThreadPool(5);
        // 异步下单购买冰箱
        Future<GoodsModel> bxFuture = executor.submit(buyGoods("冰箱", 5));
        //异步下单购买洗衣机
        Future<GoodsModel> xyjFuture = executor.submit(buyGoods("洗衣机",2));

        //关闭线程池
        executor.shutdown();

        //等待冰箱到
        GoodsModel bxGoodsModel = bxFuture.get();
        //将冰箱搬上楼
        moveUp(bxGoodsModel);
        
        //等待洗衣机
        GoodsModel xyjGoodsModel = xyjFuture.get();
        //将洗衣机搬上楼
        moveUp(xyjGoodsModel);

        long et = System.currentTimeMillis();
        System.out.println(et + "货物已经送到家啦,哈哈哈!");
        System.out.println("总耗时:" + (et - st));
    }
}

输出:

1591498163914, 开始购物!
1591498163925购买冰箱下单!
1591498163925购买洗衣机下单!
1591498163925,洗衣机-送到了!
1591498163925,冰箱-送到了!
将商品搬上楼,商品信息:冰箱,下单时间[1591498163925,1591498168928],耗时:5003
将商品搬上楼,商品信息:洗衣机,下单时间[1591498163925,1591498165930],耗时:2005
1591498178937货物已经送到家啦,哈哈哈!
总耗时:15023

从输出中我们可以看出几个时间

  1. 购买冰箱耗时5秒
  2. 购买洗衣机耗时2秒
  3. 将冰箱送上楼耗时5秒
  4. 将洗衣机送上楼耗时5秒
  5. 共计耗时15秒

购买洗衣机、冰箱都是异步执行的,我们先把冰箱送上楼了,然后再把洗衣机送上楼了。上面大家应该发现一个问题,洗衣机先到的,洗衣机到了,我们并没有把洗衣机送上楼,而是在等待冰箱到货(bxFuture.get()),然后将冰箱送上楼,中间导致浪费了3秒,现实中应该是这样的,先到的先送上楼,修改一下代码,洗衣机先到,先送洗衣机上楼。

package aboutThread.Concurrent.Day20;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    static class GoodsModel {
        // 商品名称
        String name;
        // 购物开始时间
        long starttime;
        // 送到的时间
        long endtime;

        public GoodsModel(String name, long starttime, long endtime) {
            this.name = name;
            this.starttime = starttime;
            this.endtime = endtime;
        }

        @Override
        public String toString() {
            return name + ",下单时间[" + this.starttime + "," + this.endtime + "],耗时:" + (this.endtime - this.starttime);
        }

    }

    /**
     * 将商品搬上楼
     * 
     * @param goodsModel
     * @throws InterruptedException
     */
    static void moveUp(GoodsModel goodsModel) throws InterruptedException {
        // 休眠5秒
        TimeUnit.SECONDS.sleep(5);
        System.out.println("将商品搬上楼,商品信息:" + goodsModel);
    }

    /**
     * 
     * @param name
     * @param costTime
     * @return
     */
    static Callable<GoodsModel> buyGoods(String name, long costTime) {
        return () -> {
            long startTime = System.currentTimeMillis();
            System.out.println(startTime + "购买" + name + "下单!");
            // 模拟送货耗时
            try {
                TimeUnit.SECONDS.sleep(costTime);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            System.out.println(startTime + "," + name + "-送到了!");
            return new GoodsModel(name, startTime, endTime);
        };
    }

    public static void main(String[] args) throws InterruptedException,ExecutionException{
        long st = System.currentTimeMillis();
        System.out.println(st + ", 开始购物!");

        // 创建一个线程池,用来异步下单
        ExecutorService executor = Executors.newFixedThreadPool(5);
        // 异步下单购买冰箱
        Future<GoodsModel> bxFuture = executor.submit(buyGoods("冰箱", 5));
        //异步下单购买洗衣机
        Future<GoodsModel> xyjFuture = executor.submit(buyGoods("洗衣机",2));

        //关闭线程池
        executor.shutdown();

          //等待洗衣机
          GoodsModel xyjGoodsModel = xyjFuture.get();
          //将洗衣机搬上楼
          moveUp(xyjGoodsModel);
          
        //等待冰箱到
        GoodsModel bxGoodsModel = bxFuture.get();
        //将冰箱搬上楼
        moveUp(bxGoodsModel);
        
      

        long et = System.currentTimeMillis();
        System.out.println(et + "货物已经送到家啦,哈哈哈!");
        System.out.println("总耗时:" + (et - st));
    }
}

输出:

1591512907484, 开始购物!
1591512907494购买洗衣机下单!
1591512907494购买冰箱下单!
1591512907494,洗衣机-送到了!
1591512907494,冰箱-送到了!
将商品搬上楼,商品信息:洗衣机,下单时间[1591512907494,1591512909495],耗时:2001
将商品搬上楼,商品信息:冰箱,下单时间[1591512907494,1591512912495],耗时:5001
1591512919503货物已经送到家啦,哈哈哈!
总耗时:12019

耗时12秒,比第一种少了3秒

问题来了,上面我们通过调整代码达到了最优效果,实际上,购买冰箱好洗衣机具体哪个耗时时间长我们是不知道的,怎么办呢,有没有什么解决办法?


CompletionService接口

CompletionService相当于执行任务的服务,通过submit丢任务给这个服务,服务内部去执行任务,可以通过服务提供的一些方法获取服务中已经完成的任务。

接口内的几个方法:

Future<V> submit(Callable<V> task);

用于向服务中提交有返回结果的任务,并返回Future对象

Future<V> submit(Runnable task,V result);

用户向服务中提交有返回值的任务去执行,并返回Future对象

Future<V> take() throws IngterruptedException;

从服务中返回并移除一个已经完成的任务,如果获取不到,会一直阻塞到有返回值为止。此方法会响应线程中断

Future<V> poll()

从服务中返回并移除一个已经完成的任务,如果内部没有已经完成的任务,则返回空,此方法会立即响应。

Future<V> pool(long timeout,TimeUnit unit) throws InterruptedExcetpion;

尝试在指定的时间内从服务返回并移除一个已经完成的任务,等待的时间超时还是没有获取到已经完成的任务,则返回空,此方法可以响应线程中断。

通过submit向内部提交任意多个任务,通过take方法可以获取已经执行完的任务,如果获取不到将等待。


ExecutorCompletionService类

ExecutorCompletionService类是CompletionService接口的具体实现。

说一下内部原理,ExecutorCompletionService创建的时候,会传入一个线程池,调用submit方法传入需要执行的任务,任务由内部的线程池来处理;ExecutorCompletionService内部有个阻塞队列,任意一个任务完成之后,会将任务的执行结果(Future类型)放入阻塞队列,然后其他线程可以调用它take、pool方法从这个阻塞队列中获取一个已经完成的任务, 获取任务返回结果的顺序和任务执行完成的先后顺序一致,所以最先完成的任务会先返回。

关于阻塞队列,见后续文章,加油!

看一下构造方法:

public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

构造方法需要传入一个Executor对象,这个对象表示任务执行器,所有传入的任务会被这个执行器执行。

completionQueue 是用来存储任务结果的阻塞队列,默认采用的是 LinkedBlockingQueue,也支持开发自己设置。通过submit传入需要执行的任务,任务执行完成后,会放入 completionQueue 中,有兴趣的可以看一下源码,还是很好理解的。


使用ExecutorCompletionService解决文章开头问题

 package aboutThread.Concurrent.Day20;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Demo2 {
   static class GoodsModel {
       //商品名称
       String name;
       //购物开始时间
       long starttime;
       //送到的时间
       long endtime;

       public GoodsModel(String name,long starttime,long endtime){
           this.name = name;
           this.starttime = starttime;
           this.endtime = endtime;
       }

       @Override
       public String toString() {
        return name + ",下单时间[" + this.starttime + "," + endtime + "],耗时:" + (this.endtime - this.starttime);
       }
   } 

   /**
    * 将商品搬上楼
    * @param goodsModel
    * @throws InterruptedException
    */
   static void moveUp(GoodsModel goodsModel) throws InterruptedException{
       //休眠5秒,模拟上楼耗时
       TimeUnit.SECONDS.sleep(5);
       System.out.println("将商品搬上楼,商品信息:" + goodsModel);
   }


   static Callable<GoodsModel> buyGoods(String name,long costTime){
       return () -> {
           long startTime = System.currentTimeMillis();
           System.out.println(startTime + "购买【" + name + "】下单!");
           //模拟送货耗时
           try {
               TimeUnit.SECONDS.sleep(costTime);
           } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            System.out.println(endTime + "-" + name + "送到了!");
            return new GoodsModel(name, startTime, endTime);
       };
   }

    public static void main(String[] args) throws InterruptedException,ExecutionException{
        long st = System.currentTimeMillis();
        System.out.println("开始购物!");
        ExecutorService executor = Executors.newFixedThreadPool(5);
         
        //创建ExecutorCompletionService对象
        ExecutorCompletionService<GoodsModel> executorCompletionService = new ExecutorCompletionService<>(executor);
        //异步下单买冰箱
        executorCompletionService.submit(buyGoods("冰箱", 5));
        //异步下单买洗衣机
        executorCompletionService.submit(buyGoods("洗衣机", 2));
        executor.shutdown();

        int goodsCount = 2;
        for (int i = 0; i < goodsCount; i++) {
            //可以先获取到最先到的商品
            GoodsModel goodsModel = executorCompletionService.take().get();

            //将最先到的商品送上楼
            moveUp(goodsModel);
        }

        long et = System.currentTimeMillis();
        System.out.println(et + "货物已送到家啦,哈哈哈哈!");
        System.out.println("总耗时:" + (et - st));
    }

}

输出:

开始购物!
1591515667515购买【冰箱】下单!
1591515667515购买【洗衣机】下单!
1591515669520-洗衣机送到了!
1591515672519-冰箱送到了!
将商品搬上楼,商品信息:洗衣机,下单时间[1591515667515,1591515669520],耗时:2005
将商品搬上楼,商品信息:冰箱,下单时间[1591515667515,1591515672519],耗时:5004
1591515679529货物已送到家啦,哈哈哈哈!
总耗时:12027

从输出可以看出我们希望的结果一致,代码中下单顺序是:冰箱、洗衣机,冰箱送货耗时5秒,洗衣机送货耗时2秒,洗衣机先到,然后被送上楼了,冰箱后到被送上楼,总共耗时12秒,和期望的方案一样。


示例一:执行一批任务,然后消费执行结果

package aboutThread.Concurrent.Day20;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

public class Demo3 {
    public static void main(String[] args) throws ExecutionException,InterruptedException{
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Callable<Integer>> list = new ArrayList<>();
        int taskCount = 5;
        for (int i = taskCount; i> 0; i--) {
            int j = i * 2;
            list.add(() ->{
                TimeUnit.SECONDS.sleep(j);
                return j;
            });
        }
        solve(executorService,list,a ->{
            System.out.println(System.currentTimeMillis() + ":" + a);
        });
        executorService.shutdown();
       
    }

    public static <T> void solve(Executor e,Collection<Callable<T>> solvers, Consumer<T> use) throws ExecutionException,InterruptedException{
        CompletionService<T> ecs = new ExecutorCompletionService<T>(e);
        for(Callable<T> s : solvers){
            ecs.submit(s);
        }
        int n = solvers.size();
        for (int i = 0; i < n; i++) {
            T r = ecs.take().get();
            if(r != null){
                use.accept(r);
            }
        }
    }
}

输出:

1591583960401:2
1591583962400:4
1591583964400:6
1591583966401:8
1591583968400:10

代码传入了一批任务进行处理,最终将所有处理完成的按任务完成的先后顺序传递给 Consumer 进行处理


示例:异步执行一批任务,有一个完成了,立即返回,其他的取消

这里有两种方式

方式一:

使用ExecutorCompletionService 实现,ExecutorCompletionService 提供了获取一批任务中最先完成的任务结果的能力。

代码如下:

package aboutThread.Concurrent.Day20;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Demo4 {
    public static void main(String[] args) throws InterruptedException,ExecutionException{
        long starttime = System.currentTimeMillis();
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<Callable<Integer>> list = new ArrayList<>();
        int taskCount = 5;
        for (int i = taskCount; i > 0; i--) {
            int j = i * 2; 
            String taskName = "任务" + i;
            list.add(() -> {
                TimeUnit.SECONDS.sleep(j);
                System.out.println(taskName + " - 执行完毕!");
                return j;
            });
        }
        Integer integer = invokeAny(executorService,list);
        long endtime = System.currentTimeMillis();
        System.out.println("耗时:"  + (endtime - starttime) + ",执行结果" + integer);
        executorService.shutdown();
    }

    public static <T> T invokeAny(Executor e,Collection<Callable<T>> solvers) throws InterruptedException,ExecutionException{
        CompletionService<T> ecs = new ExecutorCompletionService<T>(e);
        List<Future<T>> futureList = new ArrayList<>();
        for (Callable<T> s : solvers) {
            futureList.add(ecs.submit(s));
        }
        int n = solvers.size();
        try {
            for (int i = 0; i < n; i++) {
                T r = ecs.take().get();
                if(r != null){
                    return r;
                }
            }
        }finally {
            for(Future<T> future : futureList){
                future.cancel(true);
            }
        }
        return null;
    }
}

输出:

任务1 - 执行完毕!
耗时:2017,执行结果2

程序输出到此然后停止了。

代码中执行了5个任务,使用CompletionService 执行任务,调用take方法获取最先执行完成的任务,然后返回。在finally中对所有任务发送取消操作(future.cancel(true);),从中输出可以看出只有任务1执行成功,其他任务被成功取消了,符合预期结果。


方式二:

其实 ExecutorService 已经为我们提供了这样的方法,方法声明如下:

<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException,ExecutionException;

示例代码:

package aboutThread.Concurrent.Day20;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException,ExecutionException{
        long starttime = System.currentTimeMillis();
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        List<Callable<Integer>> list = new ArrayList<>();

        int taskCount = 5;
        for (int i = taskCount; i > 0; i--) {
            int j = i * 2;
            String taskName = "任务" + i;
            list.add(() -> {
                TimeUnit.SECONDS.sleep(j);
                System.out.println(taskName + " - 执行完毕!");
                return j;
            });
        }
        Integer integer = executorService.invokeAny(list);
        System.out.println("耗时:" + (System.currentTimeMillis() - starttime) + ",执行结果:" + integer);
        executorService.shutdown();
    }


    public static <T> T invokeAny(Executor e,Collection<Callable<T>> solvers) throws InterruptedException,ExecutionException{
        CompletionService<T> ecs = new ExecutorCompletionService<T>(e);
        List<Future<T>> futureList = new ArrayList<>();
        for (Callable<T> s : solvers) {
            futureList.add(ecs.submit(s));
        }
        int n = solvers.size();
        try {
            for (int i = 0; i < n; i++) {
                T r = ecs.take().get();
                if(r != null){
                    return r;
                }
            }
        }finally {
            for(Future<T> future : futureList){
                future.cancel(true);
            }
        }
        return null;
    }
}

输出:

任务1 - 执行完毕!
耗时:2015,执行结果:2

输出结果和方式1中结果类似。


这是java并发学习的第20天,加油!~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值