Java多线程/并发19、Callable、Future接口及CompletionService的应用

我们知道Runable()是无法直接返回值的。如果某个线程想获取另一个线程中变量的值,怎么办呢?
Java为我们提供了一种处理模式——Future模式
线程A(生产者)处理一个很耗时的工作,将会生产出一个结果。线程B(消费者)可以随时拿线程A的结果进行下一步处理。同时线程B(消费者)可以获取线程A的任务运行状态,也可以取消线程A的任务运行。
打个比喻:你到千吉买月饼,千吉收钱后给你开了一张月饼券(Future)说稍后来取,因为千吉做一盒月饼(做月饼就是Callable)需要5个小时很耗时。现在你可以随时拿这张券去换月饼,如果你一直等着,那么5小时后你就能拿到月饼。你也可以出去玩游戏,过7,8个小时侯再来取月饼也行。另外,假如你在现场等了2个小时,发现月饼还没好,这时老妈叫回家吃饭,你也可以告诉千吉,这月饼我不要了。

Future模式是一个典型的生产–>消费的模式。一个线程负责生产结果,另一个线程消费结果。
而Callable和Future一个产生结果,一个拿结果。
Callable接口类似于Runnable,但是Runnable不会返回结果,Callable可以返回结果,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。

Callable的接口定义如下;

public interface Callable<V> { 
      V   call()   throws Exception; 
} 

V是返回值类型。

Callable和Runnable的区别如下:

  • Callable定义的方法是call,而Runnable定义的方法是run。
  • Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。
  • Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。

一般,我们会用ExecutorService的submit方法执行Callable,并返回Future。

来看看买月饼的代码:

public class FutureDemo {
    public static void main(String[] args) {
        /* 定义生产者:用来做月饼的Callable */
        final Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                /*模拟耗时操作,需要5秒*/
                Thread.sleep(5000);
                /*返回一盒做好的月饼编号*/
                return new Random().nextInt(10000);
            }
        };

        /*开启线程B--消费者:获取月饼*/
        Runnable runnable=new Runnable() {
            public void run() {
                try {
                     ExecutorService tPool = Executors.newSingleThreadExecutor();
                     System.out.println("老板,给我开始做月饼...");
                     /*启动线程A--生产者:运行耗时操作,生产月饼
                      *同时返回一张月饼券CookTicket*/
                     final Future<Integer> CookTicket = tPool.submit(callable);

                     /*拿到月饼*/
                     System.out.println("5秒钟后用月饼券兑换到月饼,该盒月饼编号:"+CookTicket.get());
                     System.out.println("拿饼回家...");
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}

输出:

老板,给我开始做月饼...
5秒钟后用月饼券兑换到月饼,该盒月饼编号:652
拿饼回家...

以上线程B中的代码中可以放在主线程完成。
可以看到,线程B调用CookTicket.get()来获取线程A返回的数据,程序首先输出“老板,给我开始做月饼…”,等待5秒后获得线程A生产的结果。CookTicket.get()会阻塞当前线程B,等待线程A运行完毕后再输出结果。


假设我现在同时要3盒月饼时怎么办?千吉说我可以开3条生产线为你制作,再给你3张Future券。
这里我们可以用集合来实现,改写一下线程B运行的runable代码

/* 开启线程B--消费者:获取月饼 */
        Runnable runnable = new Runnable() {
            public void run() {
                try {
                    System.out.println("老板,给我开始做月饼...");
                    /*用缓存线程池同时开多个线程工作*/
                    ExecutorService tPool = Executors.newCachedThreadPool();
                    /*定义月饼券集合*/
                    List<Future<Integer>> futures=new ArrayList<Future<Integer>>();
                    /*启动线程A--生产者:运行耗时操作,三条生产线开始生产月饼*/
                    for(int i=0;i<3;i++){
                        Future<Integer> tFuture=tPool.submit(callable);
                        futures.add(tFuture);
                    }
                    System.out.println("等待5秒钟....");

                    /* 拿月饼 --消费结果*/
                    for(Future<Integer> ft:futures){
                        System.out.println("用月饼券兑换到月饼,该月饼编号:"+ ft.get());
                        }
                    System.out.println("拿饼回家...");

                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };

我们开启了三个线程来执行任务。在获取任务结果时,遍历list取出future,但这是按照将future添加到list的顺序来取出的。在多线程中,先启动的线程并不一定先完成,我们无从知晓哪个任务最先结束。所以取结果时,future.get()会阻塞以等待第一个线程执行完毕返回的future,但这时有可能第二、三线程早已执行完成,所以这样并不合理。

future有个方法isDone()用来判断future是否执行完成拿到结果的,并且它不会阻塞当前线程。因此我们思路是:循环扫描List对象,如果其中的future已完成,就马上执行。如此循环,直到完成所有List中的future的结果获取。我们继续修改“拿月饼”这一段:

/* 拿月饼 --消费结果*/
Boolean flag=true;
while(flag){
    for(Future<Integer> ft:futures){
        if(ft.isDone()){
            System.out.println("用月饼券兑换到月饼,该月饼编号:"+ ft.get());
            futures.remove(ft);
            break;//break很关键,因为遍历时改动了list
        }
    }
    if(futures.size()==0){
            flag=false;
    }
}

至此,哪个线程先完成,就能先输出结果。不过实现起来太繁琐了,好在JAVA为我们提供了一个更好的机制:CompletionService


CompletionService的实现是维护一个保存Future对象的BlockingQueue(阻塞的FIFO 队列,见《阻塞队列BlockingQueue》)。只有当这个Future对象状态是完成的时候,才会加入到这个Queue中,先完成的先加入。由于是队列,用它的方法take()取数据时遵守先进先出原则。所以,先完成的必定也是先被取出。
代码可以变得更简洁:

public class FutureDemo {
    public static void main(String[] args) {
        /* 定义生产者:用来做月饼的Callable */
        final Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                /* 模拟耗时操作,需要5秒 */
                Thread.sleep(5000);
                /* 返回一盒做好的月饼编号 */
                return new Random().nextInt(10000);
            }
        };

        /* 开启线程B--消费者:获取月饼 */
        Runnable runnable = new Runnable() {
            public void run() {
                try {
                    System.out.println("老板,给我开始做月饼...");
                    /*用缓存线程池可同时开多个线程工作*/
                    ExecutorService tPool = Executors.newCachedThreadPool();
                    /*定义领多盒的月饼券*/
                    CompletionService<Integer> CookCompletion = new ExecutorCompletionService<Integer>(tPool);
                    /*启动线程A--生产者:运行耗时操作,三条生产线开始生产月饼*/
                    for(int i=0;i<3;i++){
                        CookCompletion.submit(callable);
                    }
                    System.out.println("等待5秒钟....");
                    /* 拿到月饼 */
                    for(int i=0;i<3;i++){
                        System.out.println("用月饼券兑换到月饼,该月饼编号:"+ CookCompletion.take().get());
                    }
                    System.out.println("拿饼回家...");
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(runnable).start();
    }
}

输出:

老板,给我开始做月饼...
等待5秒钟....
用月饼券兑换到月饼,该月饼编号:8708
用月饼券兑换到月饼,该月饼编号:8057
用月饼券兑换到月饼,该月饼编号:4188
拿饼回家...

总结:
Future接口中有如下方法:

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值