FutureTask和CompletionService的使用

带返回值的线程

传统的创建线程的两种方法

  1. 实现Runnable接口
  2. 继承Thread方法然后重写run方法

这两种方法创建的线程都没有带有返回值.有时候我们需要线程带返回值过来.则可以通过实现Callable接口,配合FutureTask或者executors来实现.看简单的一个计算阶乘10!的方法

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

        FutureTask<Integer> futureTask=new FutureTask(new Factorial(10));

        Thread t=new Thread(futureTask);
        t.start();
        //调用get方法获取线程计算的返回值,调用get会阻塞
        System.out.println(futureTask.get());

    }
   static class Factorial implements Callable<Integer>{
        private int n;

       public Factorial(int n) {
           this.n = n;
       }

       @Override
        public Integer call() throws Exception {
            if(n==0){
                return 1;
            }
            int num=n;
            for (int i = 1; i < n; i++) {
                num*=i;
            }
            return num;
        }
    }
}

复制代码

可以观察到它是new一个FutureTask对象,FutureTask对象需要一个实现Callable接口的对象,然后再交由一个线程来执行. 而我们编写的计算逻辑在call方法中.

进入FutureTask观察其源码可以发现它实现了RunnableFuture接口,而RunnableFuture接口又继承自Runnable接口和Future接口,所以它其实也就需要实现run()方法. 查看它的run方法便可以得知,它在run方法中其实也就调用了Callable接口的call方法,并获取返回值,设置到 outcome属性中,然后通过调用get方法来返回该值.

比较常用的是通过线程池来提交任务.

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //线程池传递一个实现Callable接口的实现类
        Future<Integer> submit = executorService.submit(new Factorial(10));

        System.out.println(submit.get());
        executorService.shutdown();&emsp;//关闭线程池

    }
复制代码

查看源码可观察到,它内部也是将Callable封装到FutureTask中,然后调用线程池中的线程来执行FutureTask.

 public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
 protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }
复制代码

CompletionService的使用

假设现在有一个网页,我们需要下载图片并显示出来给用户看,最高效率的做法就是每下载完一张图片就显示一张,其它未下载完成的占一个空间等待渲染.用FutureTask如何实现,我们用一个HashMap来模拟图片,key代表图片,value代表下载的时间(秒).

final Map<String,Integer> map=new LinkedHashMap<>();//确保插入有序
        map.put("1.jpg",5);
        map.put("2.jpg",3);
        map.put("3.jpg",7);
        map.put("4.png",1);
复制代码

我们假设为每张图片创建一个下载任务,我们用sleep来模拟下载时间,那么代码应该这么写.

 LinkedBlockingQueue<Future<String>> s= new LinkedBlockingQueue<>();//用于保存Future

ExecutorService service = Executors.newFixedThreadPool(4);//创建一个线程池
for (String s : map.keySet()) {&emsp;&emsp;//遍历每张图片,并提交任务
Future<String> future= service.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(map.get(s)*1000); //模拟下载中
                    return s; //下载完成返回这张图片
                }
            });
           s.add(future); 
        }
        //从阻塞队列中取的Future获取结果
 for (int i = 0; i < 4; i++) {
            Future<String> poll = null;
            try {
                poll = s.take();
                System.out.println(poll.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
output:     
1.jpg
2.jpg
3.jpg
4.png

复制代码

获取到future然后保存起来,在外面进行get()获取结果,但是很明显这个不符合我们想要的,我们想要的是先下载完成的立马可以显示出来,很明显4.png下载才需要1秒应该第一显示,上面的思路逻辑明显不符合要求.
我们需要的是当这个任务完成时立马被设置到阻塞队列中.

传统线程启动

查看FutureTask源码,可以知道它有一个done()方法,默认方法体为空,当完成设置结果后,它会调用done()方法.所以我们的任务就是重写这个done方法.然而非常的麻烦,假设不用线程池来还好,那么只需要新建一个FutureTask的子类然后重写done方法

class QueenFutureTask<T> extends FutureTask<T>{
        public QueenFutureTask(Callable<T> callable) {
            super(callable);
        }
        public QueenFutureTask(Runnable runnable, T result) {
            super(runnable, result);
        }

        @Override
        protected void done() {
            try {
                block.add((String) this.get());&emsp;//把下载完的图片加入到阻塞队列中
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }

复制代码

然后用传统线程来启动即可

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        final Map<String,Integer> map=new LinkedHashMap<>();
        map.put("1.jpg",5);
        map.put("2.jpg",3);
        map.put("3.jpg",7);
        map.put("4.png",1);

        for (String s : map.keySet()) {
            new Thread(new QueenFutureTask<String>(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(map.get(s) * 1000);
                    return s;
                }
            }) {

            }).start();
        }
            for (int i = 0; i < 4; i++) {
                String take = block.take();
                System.out.println(take);
            }
    }
output:
4.png
2.jpg
1.jpg
3.jpg
复制代码

用线程池也行,不过很麻烦,因为你查看源代码就可以得知,默认实现是通过新建一个FutureTask来执行,而我们又无法直接重写FutureTask,只能使用的QueenFutureTask,所以你还得重写线程池的newTaskFor方法(代码就不贴了,太长).所幸jdk提供了一个CompletionService类(终于进入正题),将Executor和BlockingQueue功能融合

CompletionService的使用

它将提交的任务实现完成后放入阻塞队列中,使用非常简单,自己看吧

public static void main(String[] args) throws InterruptedException, ExecutionException {
        final Map<String,Integer> map=new LinkedHashMap<>();
        map.put("1.jpg",5);
        map.put("2.jpg",3);
        map.put("3.jpg",7);
        map.put("4.png",1);
        ExecutorService service = Executors.newFixedThreadPool(4);
        
        //需要传一个线程池
        CompletionService<String> completionService=new ExecutorCompletionService<>(service);

        for (String s : map.keySet()) {
            completionService.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    Thread.sleep(map.get(s)*1000);
                    return s;
                }
            });
        }

        for (int i = 0; i < map.size(); i++) {
            Future<String> take = completionService.take();&emsp;
            String s = take.get();
            System.out.println(s);
        }

        service.shutdown();
    }
output:
4.png
2.jpg
1.jpg
3.jpg
复制代码

猜你喜欢

转载自juejin.im/post/5d985dbb6fb9a04e0a37e7ec