Future(FutureTask)及CompletionService的使用

160 阅读9分钟

一、Callable&Future&FutureTask介绍

Java项目编程中,为了充分利用计算机CPU资源,一般开启多个线程来执行异步任务,但是不论继承Thread还是实现Runnable接口都无法获取任务执行的结果,因此java1.5就提供了Callable接口来实现这一场景,而Future和FutureTask就可以和Callable接口配合起来使用。

1-1、Callable和Runnable的区别

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Runnable 的缺陷:

  • 不能返回一个返回值
  • 不能抛出 checked Exception

Callable的call方法可以有返回值,可以声明抛出异常。和 Callable 配合的有一个 Future 类,通过 Future 可以了解任务执行情况,或者取消任务的执行,还可获取任务执行的结果,这些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 强大。

@Slf4j
public class FutureDemo {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("普通线程执行");
            }
        }).start();

        new Thread(()->{
            log.info("Lambda方式线程执行");
        }).start();


        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                System.out.println("通过Callable方式执行任务");
                Thread.sleep(3000);
                return "返回任务结果";
            }
        });
        log.info("a:"+task.isDone());
        new Thread(task).start();
        log.info("b:"+task.isDone());
        log.info("执行结果:{}",task.get());
        log.info("c:"+task.isDone());
    }
}

执行结果:

image.png 通过上图可以看到通过使用FutureTask,然后传入Callable,就可以通过get()获得线程执行的结果,通过isDone()就可以获得线程是否执行完毕。

1-2、Future 的主要功能

image.png

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

  • boolean cancel (boolean mayInterruptIfRunning) 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
  • boolean isCancelled () 任务是否已经取消,任务正常完成前将其取消,则返回 true
  • boolean isDone () 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true
  • V get () throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出CancellationException
  • V get (long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException

image.png

通过以上的了解,我们就可以试着用FutureTask做一个多线程并行的操作,比如我们要泡茶,如果没有多线程的方式,

1、我们使用单线程的方式泡茶顺序如下:
洗杯子->放茶叶->烧水->倒水

2、使用多线程的方式泡茶:
烧水
洗杯子->放茶叶->获得水是否烧开(烧开)->倒水

通过下面多线程的方式,我们就可以在烧水的时候去洗杯子,放茶叶,这样就可以让多个任务并行操作。

1-2-1、根据FutureTask原理实现自己的FutureTask

通过上面FutureTask的实现过程,可以看出来get是一个阻塞的获取结果方法,当有结果的时候即可以返回执行的结果,根据这个机制,我们自己写一个FutureTask

@Slf4j
public class MyFutureTask<V> implements Runnable, Future {
    private Callable<V> callable;
    private V result = null;

    public MyFutureTask(Callable<V> callable) {
        this.callable = callable;
    }

    @Override
    public void run() {
        try {
            result=callable.call();
            synchronized (this){
                this.notify();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        return false;
    }

    @Override
    public boolean isCancelled() {
        return false;
    }

    @Override
    public boolean isDone() {
        return this.result!=null;
    }

    @Override
    public Object get() throws InterruptedException, ExecutionException {
        if(result!=null){
            return result;
        }else{
            synchronized (this){
                this.wait();
            }
        }
        return result;
    }

    @Override
    public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        if(result!=null){
            return result;
        }else {
            if(timeout>0L){
                unit.sleep(timeout);
                if (result!=null){
                    return result;
                }else{
                    throw new TimeoutException();
                }
            }
            return result;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyFutureTask task = new MyFutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                Thread.sleep(3000);
                return "返回任务结果";
            }

        });

        new Thread(task).start();
        log.debug("结果:{}",task.get());
    }
}

执行结果:

image.png

1-3、利用 FutureTask 创建 Future

Future实际采用FutureTask实现,该对象相当于是消费者和生产者的桥梁,消费者通过 FutureTask 存储任务的处理结果,更新任务的状态:未开始、正在处理、已完成等。而生产者拿到的 FutureTask 被转型为 Future 接口,可以阻塞式获取任务的处理结果,非阻塞式获取任务处理状态。

FutureTask既可以被当做Runnable来执行,也可以被当做Future来获取Callable的返回结果

image.png

1-3-1、使用案例:促销活动中商品信息查询

在维护促销活动时需要查询商品信息(包括商品基本信息、商品价格、商品库存、商品图片、商品销售状态等)。这些信息分布在不同的业务中心,由不同的系统提供服务。如果采用同步方式,假设一个接口需要50ms,那么一个商品查询下来就需要200ms-300ms,这对于我们来说是不满意的。如果使用Future改造则需要的就是最长耗时服务的接口,也就是50ms左右。

image.png

public class FutureTaskDemo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        FutureTask<String> ft1 = new FutureTask<>(new T1Task());
        FutureTask<String> ft2 = new FutureTask<>(new T2Task());
        FutureTask<String> ft3 = new FutureTask<>(new T3Task());
        FutureTask<String> ft4 = new FutureTask<>(new T4Task());
        FutureTask<String> ft5 = new FutureTask<>(new T5Task());

        //构建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.submit(ft1);
        executorService.submit(ft2);
        executorService.submit(ft3);
        executorService.submit(ft4);
        executorService.submit(ft5);
        //获取执行结果
        System.out.println(ft1.get());
        System.out.println(ft2.get());
        System.out.println(ft3.get());
        System.out.println(ft4.get());
        System.out.println(ft5.get());

        executorService.shutdown();

    }

    static class T1Task implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("T1:查询商品基本信息...");
            TimeUnit.MILLISECONDS.sleep(5000);
            return "商品基本信息查询成功";
        }
    }

    static class T2Task implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("T2:查询商品价格...");
            TimeUnit.MILLISECONDS.sleep(50);
            return "商品价格查询成功";
        }
    }

    static class T3Task implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("T3:查询商品库存...");
            TimeUnit.MILLISECONDS.sleep(50);
            return "商品库存查询成功";
        }
    }

    static class T4Task implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("T4:查询商品图片...");
            TimeUnit.MILLISECONDS.sleep(50);
            return "商品图片查询成功";
        }
    }

    static class T5Task implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("T5:查询商品销售状态...");
            TimeUnit.MILLISECONDS.sleep(50);
            return "商品销售状态查询成功";
        }
    }
}

执行结果:

image.png

给任务1设置2秒超时时间,再次执行结果如下:

image.png

通过以上我们总结出来:虽然FutureTask能让多个任务同时执行,但是缺点是如果5个任务同时执行,第一个任务要等待很久,那任务就会卡在第一个任务,导致结果无法返回,这在一些实际场景中是不允许的。我们可以设置一个超时时间,这样就不需要一直等待下去。

而有些业务场景即使其中某一个某几个任务无法返回,那其他任务也需要即时的返回执行结果,这样就可以使用 CompletionService 这个后面再进行使用

1-4、Future 注意事项

  • 当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制
  • Future 的生命周期不能后退。一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来

1-5、Future的局限性

从本质上说,Future表示一个异步计算的结果。它提供了isDone()来检测计算是否已经完成,并且在计算结束后,可以通过get()方法来获取计算结果。在异步计算中,Future确实是个非常优秀的接口。但是,它的本身也确实存在着许多限制:

  • 并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待你别无他法;
  • 无法对多个任务进行链式调用 :如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
  • 无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
  • 没有异常处理:Future接口中没有关于异常处理的方法;

二、CompletionService

上面提到如果多个任务,有个别任务无法获得执行结果,那么就需要将已经获得执行结果的值进行返回,FutureTask无法做到这一点,CompletionService就可以实现上面的需求

Callable+Future 可以实现多个task并行执行,但是如果遇到前面的task执行较慢时需要阻塞等待前面的task执行完后面task才能取得结果。而CompletionService的主要功能就是一边生成任务,一边获取任务的返回值。让两件事分开执行,任务之间不会互相阻塞,可以实现先执行完的先取结果,不再依赖任务顺序了

image.png

2-1、CompletionService原理

内部通过阻塞队列+FutureTask,实现了任务先完成可优先获取到,即结果按照完成先后顺序排序,内部有一个先进先出的阻塞队列,用于保存已经执行完成的Future,通过调用它的take方法或poll方法可以获取到一个已经执行完成的Future,进而通过调用Future接口实现类的get方法获取最终的结果

2-2、询价示例

下面以一个询价的示例来进行讲述,哪个询价结果先返回,就先进行保存哪个

@Slf4j
public class CompletionServiceDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {

        //创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);
        //创建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++) {
            //从阻塞队列获取futureTask
            Integer r = cs.take().get();
            executor.execute(() -> save(r));
        }


        executor.shutdown();
    }

    private static void save(Integer r) {
        log.debug("保存询价结果:{}",r);
    }

    private static Integer getPriceByS1() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(5000);
        log.debug("电商S1询价信息1200");
        return 1200;
    }
    private static Integer getPriceByS2() throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(8000);
        log.debug("电商S2询价信息1000");
        return 1000;
    }
    private static Integer getPriceByS3()  throws InterruptedException {
        TimeUnit.MILLISECONDS.sleep(3000);
        log.debug("电商S3询价信息800");
        return 800;
    }
}

执行结果:

image.png

2-3、应用场景总结

  • 当需要批量提交异步任务的时候建议你使用CompletionService。CompletionService将线程池Executor和阻塞队列BlockingQueue的功能融合在了一起,能够让批量异步任务的管理更简单。
  • CompletionService能够让异步任务的执行结果有序化。先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如Forking Cluster这样的需求。
  • 线程池隔离。CompletionService支持自己创建线程池,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险