Java——聊聊JUC中的Future和FutureTask

Posted 宋子浩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java——聊聊JUC中的Future和FutureTask相关的知识,希望对你有一定的参考价值。

1.回顾Future和FutureTask

首先,Future是一个接口(Java5就诞生了),而FutureTask是它的实现类。而Future接口则是定义了操作异步任务执行的一些方法,提供了一种异步并行计算的功能,例如:获取异步任务的执行结果、取消异步任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

而异步任务指的是:如果主线程需要执行一个很耗时的计算任务,我们就可以通过Future把这个任务放到异步线程中执行,那么此时主线程继续处理其他任务,异步线程处理完成后,再通过Future获取计算结果。(也即三特点:①多线程;②有返回值;③异步任务)

那么我们为什么需要异步任务呢?我把所有的任务工作都放在main主线程中不行吗? 举个简单的栗子:比如我现在在球场上打球,但是没有小姐姐来观战,我觉得打的没啥意思,但是我要进攻又要抢篮板,没空啊,所以我这个时候可以让场下的替补队员去帮我找点小姐姐来看我打球,这样我继续打球,替补队员去做他的工作,效率自然就提高了啊。(这栗子并不是说替补队员就是干这事的啊,哈哈哈)   在这里,main主线程就是打球,异步线程就是找小姐姐。

1.1 最简单的例子认识Future和FutureTask

package com.szh.demo;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyThread implements Callable<String> 
    @Override
    public String call() throws Exception 
        System.out.println(Thread.currentThread().getName() + " come in call()....");
        return "hello Callable";
    


public class FutureTaskDemo 
    public static void main(String[] args) throws ExecutionException, InterruptedException 
        FutureTask<String> futureTask = new FutureTask<>(new MyThread());
        Thread thread = new Thread(futureTask, "t1");
        thread.start();
        System.out.println(futureTask.get());
    

1.2 Future优点:结合线程池,一定程度上提高程序执行效率

我这里懒省事,就使用了 Executors.newXXX 这种方式。那么熟悉阿里巴巴Java开发规范以及线程池相关原理的,都知道,不建议这样去创建线程池,而是使用ThreadPoolExecutor详细指定线程池的几大参数去创建。

package com.szh.demo;

import java.util.concurrent.*;

public class FutureTaskDemo2 
    public static void main(String[] args) throws ExecutionException, InterruptedException 
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        long startTime = System.currentTimeMillis();
        FutureTask<String> futureTask1 = new FutureTask<>(() -> 
            try 
                TimeUnit.MILLISECONDS.sleep(500);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            return "task1 over";
        );
        threadPool.submit(futureTask1);

        FutureTask<String> futureTask2 = new FutureTask<>(() -> 
            try 
                TimeUnit.MILLISECONDS.sleep(300);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            return "task2 over";
        );
        threadPool.submit(futureTask2);

//        System.out.println(futureTask1.get());
//        System.out.println(futureTask2.get());

        try 
            TimeUnit.MILLISECONDS.sleep(300);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        long endTime = System.currentTimeMillis();
        System.out.println("cost time : " + (endTime - startTime) + " ms");

        System.out.println(Thread.currentThread().getName() + " --- end");
        threadPool.shutdown();
    

将代码中的两行 get 注释打开之后,就可以通过Future 获取到异步任务的执行结果了,如下:↓↓↓ 

 这里我们使用Future结合线程池完成了多任务配合,如果我们不使用多线程,将上述代码转换成单线程了话,那么执行时间大概就是 500 + 300 + 300 = 1100 ms左右,对比Future加线程池的 861 ms,还是显著的提高了程序的执行效率。

1.3 Future缺点:get()阻塞 & isDone()轮询

Future虽然可以获取到异步线程的执行结果,就是通过get()方法,但是这个方法会阻塞其他线程的,↓↓↓

package com.szh.demo;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * public V get() 方法容易阻塞,一般建议放在程序后面,一旦调用就会等到拿到线程执行结果才会离开
 * 如果不愿意等待太长时间,我希望过期不候,可以自动离开,则使用 public V get(long timeout, TimeUnit unit)
 * 实际开发中这两个方法都不建议使用
 */
public class FutureTaskDemo3 
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException 
        FutureTask<String> futureTask = new FutureTask<>(() -> 
            System.out.println(Thread.currentThread().getName() + " come in....");
            try 
                TimeUnit.SECONDS.sleep(5);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            return "task over";
        );
        Thread t1 = new Thread(futureTask, "t1");
        t1.start();

        System.out.println(Thread.currentThread().getName() + " 忙其他任务了....");
        System.out.println(futureTask.get());
        //System.out.println(futureTask.get(3, TimeUnit.SECONDS));
//        while (true) 
//            if (futureTask.isDone()) 
//                System.out.println(futureTask.get());
//                break;
//             else 
//                try 
//                    TimeUnit.MILLISECONDS.sleep(500);
//                 catch (InterruptedException e) 
//                    e.printStackTrace();
//                
//                System.out.println("正在处理中,请稍后....");
//            
//        
    

这里程序会首先打印出前两行,而最后一行我们是在main主线程中去获取异步线程的执行结果的,而异步线程计算结果需要5秒,所以这里的main主线程就会被阻塞5秒才可以拿到这个结果。 

将代码的 System.out.println(futureTask.get(3, TimeUnit.SECONDS)); 注释打开,它表示我main主线程最多等待你异步线程3秒,3秒之后,无论你异步线程是否将结果计算完毕,我TM都不管你了,我直接暴力结束(过期不候)。↓↓↓

将上面两个get方法注释掉,打开最后的while循环体,这里我们就采用了 isDone() 轮询的方式,但是这种方式的缺点就是:轮询的方式会耗费无谓的CPU资源,而且也不见得能及时得到异步线程的执行结果。↓↓↓↓↓↓

 所以综上所述,Future虽然可以获取到异步线程的执行结果,但是它对结果的获取行为都不是很友好(要么阻塞、要么不断轮询耗费CPU资源)。


2.总结

有了上面这几个代码案例,我们对Future、FutureTask结合线程池就有了一定的了解,那么对于简单的业务场景使用这样的方式是完全OK的,但是对于一些较复杂的场景、电商网站的高并发就不行了。

我们可能就需要:

  • 回调通知(应对Future中异步线程的完成时间,完成了你再告诉主线程,不要让主线程一直阻塞等你)。
  • 想将多个异步任务的计算结果组合起来,后一个异步任务的计算结果需要前一个异步任务的执行结果;将两个或多个异步计算合成一个异步计算,这几个异步计算是相互独立的,同时后面这个又依赖前一个处理的结果。
  • 对多个异步线程执行任务的过程中,我们要获取执行任务最快的那个异步线程的计算结果。

3.引出CompletableFuture

上面已经总结了Future的种种缺点,由此就引出了Java8的异步编程利器:CompletableFuture。

我们可以看一下这二者的API数量对比:↓↓↓

 

这一对比,卧槽,天差地别了。。。就一句话:Future能干的,我 CompletableFuture 照样能干。

关于 CompletableFuture :Java——聊聊JUC中的CompletableFuture

以上是关于Java——聊聊JUC中的Future和FutureTask的主要内容,如果未能解决你的问题,请参考以下文章

Java——聊聊JUC中的Future和FutureTask

Java——聊聊JUC中的CompletableFuture

Java——聊聊JUC中的CompletableFuture

Java——聊聊JUC中的CompletableFuture

Java多线程系列--“JUC线程池”06之 Callable和Future

Java - "JUC线程池" Callable与Future