干货!并发编程——Future

694 阅读11分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

Future,未来,未来无限可能,用这句话作为开场白吧。 这是并发编程的第四篇文章了。感谢阅读,并希望以后持续关注,我会输出更多技术干货,我们共同进步!

以后可能会分为几大专题,类似于并发专题,源码专题,面试专题等(只会分享干货)。

请出主角:Future

Q: Future是什么,有什么作用?

  • Future 最主要的作用是,比如当做一定运算的时候,运算过程可能比较耗时,有时会去查数据库,或是繁重的计算,比如压缩、加密等,在这种情况下,如果我们一直在原地等待方法返回,显然是不明智的,整体程序的运行效率会大大降低。我们可以把运算的过程放到子线程去执行,再通过 Future 去控制子线程执行的计算过程,最后获取到计算结果。这样一来就可以把整个程序的运行效率提高,是一种异步的思想。

  • Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作

  • 举个例子:比如去吃早点时,点了包子和凉菜,包子需要等3分钟,凉菜只需1分钟,如果是串行的一个执行,在吃上早点的时候需要等待4分钟。那Future这种模式就是你在等包子的时候,可以同时准备凉菜,所以在准备凉菜的过程中,可以同时准备包子,这样只需要等待3分钟

Q:为什么不用多线程,照样可以异步

  • 在并发编程中,我们经常用到非阻塞的模型,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果

讲Future必须先提Callable

上面我们已经提到了,有两种创建线程的方法:一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable接口。

Callable 是一个类似于 Runnable 的接口,实现 Callable 接口的类和实现 Runnable 接口的类都是可以被其他线程执行的任务。 我们看一下 Callable 的源码:

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

可以看出它也是一个 interface,并且它的 call 方法中已经声明了 throws Exception,前面还有一个 V 泛型的返回值,实现 Callable 接口,就要实现 call 方法,这个方法的返回值是泛型 V,如果把 call 中计算得到的结果放到这个对象中,就可以利用 call 方法的返回值来获得子线程的执行结果了。

我们看一下 Runnable 的源码

public interface Runnable {
   public abstract void run();
}

明显,返回值是void。

Callable 和 Future 的关系

前面讲过,Callable 接口相比于 Runnable 的一大优势是可以有返回结果,那这个返回结果怎么获取呢?就可以用 Future 类的 get 方法来获取 。因此,Future 相当于一个存储器,它存储了 Callable 的 call 方法的任务结果。除此之外,我们还可以通过 Future 的 isDone 方法来判断任务是否已经执行完毕了,还可以通过 cancel 方法取消这个任务,或限时获取任务的结果等,总之 Future 的功能比较丰富。有了这样一个从宏观上的概念之后,我们就来具体看一下 Future 类的主要方法。


public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutExceptio
}

其中,第 5 个方法是对第 4 个方法的重载,方法名一样,但是参数不一样。

get() 方法:获取结果

  • get 方法最主要的作用就是获取任务执行的结果,该方法在执行时的行为取决于 Callable 任务的状态。

下面用图的形式让过程更清晰:

image.png

在图中,右侧是一个线程池,线程池中有一些线程来执行任务。重点在图的左侧,可以看到有一个 submit 方法,该方法往线程池中提交了一个 Task,这个 Task 实现了 Callable 接口,当我们去给线程池提交这个任务的时候,调用 submit 方法会立刻返回一个 Future 类型的对象,这个对象目前内容是空的,其中还不包含计算结果,因为此时计算还没有完成。

当计算一旦完成时,也就是当我们可以获取结果的时候,线程池便会把这个结果填入到之前返回的 Future 中去(也就是 f 对象),而不是在此时新建一个新的 Future。这时就可以利用 Future 的 get 方法来获取到任务的执行结果了。

代码示例

我们拿上面那个吃早点的例子演示代码:

需求场景:等早餐过程中,包子需要3秒,凉菜需要1秒,普通的多线程需要四秒才能完成。先等凉菜,再等包子,因为等凉菜时,普通多线程启动start()方法,执行run()中具体方法时,没有返回结果,所以如果要等有返回结果,必须是要1秒结束后才知道结果。

普通多线程

public class BumThread extends Thread{
    
    @Override
    public void run() {
        try {
            Thread.sleep(1000*3);
            System.out.println("包子准备完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ColdDishThread extends Thread{

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println("凉菜准备完毕");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        
        // 等凉菜 -- 必须要等待返回的结果,所以要调用join方法
        Thread t1 = new ColdDishThread();
        t1.start();
        t1.join();
        
        // 等包子 -- 必须要等待返回的结果,所以要调用join方法
        Thread t2 = new BumThread();
        t2.start();
        t2.join();
        
        long end = System.currentTimeMillis();
        System.out.println("准备完毕时间:"+(end-start));
    }

采用Future模式


public static void main(String[] args) throws InterruptedException, ExecutionException {
        long start = System.currentTimeMillis();
        
        // 等凉菜 
        Callable ca1 = new Callable(){
 
            @Override
            public String call() throws Exception {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "凉菜准备完毕";
            }
        };
        FutureTask<String> ft1 = new FutureTask<String>(ca1);
        new Thread(ft1).start();
        
        // 等包子 -- 必须要等待返回的结果,所以要调用join方法
        Callable ca2 = new Callable(){
 
                @Override
                public Object call() throws Exception {
                    try {
                        Thread.sleep(1000*3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    return "包子准备完毕";
            }
        };
        FutureTask<String> ft2 = new FutureTask<String>(ca2);
        new Thread(ft2).start();
        
        System.out.println(ft1.get());
        System.out.println(ft2.get());
        
        long end = System.currentTimeMillis();
        System.out.println("准备完毕时间:"+(end-start));
    }

显而易见,Future模式更胜一筹!

还没懂,还没看够代码示例,那就再来一组

针对每种方法函数展示代码(深入剖析):


/**
 * 描述 :演示一个 经典Future 的使用方法
 */
public class OneFuture {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        Future<Integer> future = service.submit(new CallableTask());
        try {
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }

    static class CallableTask implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            Thread.sleep(3000);
            return new Random().nextInt();
        }
    }
}

在这段代码中,main 方法新建了一个 10 个线程的线程池,并且用 submit 方法把一个任务提交进去。这个任务如代码的最下方所示,它实现了 Callable 接口,它所做的内容就是先休眠三秒钟,然后返回一个随机数。接下来我们就直接把 future.get 结果打印出来,其结果是正常打印出一个随机数,比如 100192 等。这段代码对应了我们刚才那个图示的讲解,这也是 Future 最常用的一种用法。

isDone() 方法:判断是否执行完毕:

下面我们再接着看看 Future 的一些其他方法,比如说 isDone() 方法,该方法是用来判断当前这个任务是否执行完毕了。

需要注意的是,这个方法如果返回 true 则代表执行完成了;如果返回 false 则代表还没完成。但这里如果返回 true,并不代表这个任务是成功执行的,比如说任务执行到一半抛出了异常。那么在这种情况下,对于这个 isDone 方法而言,它其实也是会返回 true 的,因为对它来说,虽然有异常发生了,但是这个任务在未来也不会再被执行,它确实已经执行完毕了。所以 isDone 方法在返回 true 的时候,不代表这个任务是成功执行的,只代表它执行完毕了。

我们用一个代码示例来看一看,代码如下所示:


public class GetException {

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(20);
        Future<Integer> future = service.submit(new CallableTask());


        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(i);
                Thread.sleep(500);
            }
            System.out.println(future.isDone());
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }


    static class CallableTask implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            throw new IllegalArgumentException("Callable抛出异常");
        }
    }
}

在这段代码中,可以看到有一个线程池,并且往线程池中去提交任务,这个任务会直接抛出一个异常。那么接下来我们就用一个 for 循环去休眠,同时让它慢慢打印出 0 ~ 4 这 5 个数字,这样做的目的是起到了一定的延迟作用。在这个执行完毕之后,再去调用 isDone() 方法,并且把这个结果打印出来,然后再去调用 future.get()。

这段代码的执行结果是这样的:

0
1
2
3
4
true
java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Callable抛出异常
...

这里要注意,我们知道这个异常实际上是在任务刚被执行的时候就抛出了,因为我们的计算任务中是没有其他逻辑的,只有抛出异常。我们再来看,控制台是什么时候打印出异常的呢?它是在 true 打印完毕后才打印出异常信息的,也就是说,在调用 get 方法时打印出的异常。

这段代码证明了三件事情:第一件事情,即便任务抛出异常,isDone 方法依然会返回 true;第二件事情,虽然抛出的异常是 IllegalArgumentException,但是对于 get 而言,它抛出的异常依然是 ExecutionException;第三个事情,虽然在任务执行一开始时就抛出了异常,但是真正要等到我们执行 get 的时候,才看到了异常。

isCancelled() 方法:判断是否被取消

最后一个方法是 isCancelled 方法,判断是否被取消,它和 cancel 方法配合使用,比较简单。

用 FutureTask 来创建 Future

上面早点当中的例子已经很鲜明了,这里再来点代码帮助大家更深入下:

除了用线程池的 submit 方法会返回一个 future 对象之外,同样还可以用 FutureTask 来获取 Future 类和任务的结果。

FutureTask 首先是一个任务(Task),然后具有 Future 接口的语义,因为它可以在将来(Future)得到执行的结果。

我们来看一下 FutureTask 的代码实现:


public class FutureTask<V> implements RunnableFuture<V>{
 ...
}

可以看到,它实现了一个接口,这个接口叫作 RunnableFuture。我们再来看一下 RunnableFuture 接口的代码实现:


public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

可以看出,它是 extends Runnable 和 Future 这两个接口的,它们的关系如下图所示:

image.png

既然 RunnableFuture 继承了 Runnable 接口和 Future 接口,而 FutureTask 又实现了 RunnableFuture 接口,所以 FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。

典型用法是,把 Callable 实例当作 FutureTask 构造函数的参数,生成 FutureTask 的对象,然后把这个对象当作一个 Runnable 对象,放到线程池中或另起线程去执行,最后还可以通过 FutureTask 获取任务执行的结果。

下面我们就用代码来演示一下:


/**
 * 描述:     演示 FutureTask 的用法
 */
public class FutureTaskDemo {

    public static void main(String[] args) {
        Task task = new Task();
        FutureTask<Integer> integerFutureTask = new FutureTask<>(task);
        new Thread(integerFutureTask).start();

        try {
            System.out.println("task运行结果:"+integerFutureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class Task implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        System.out.println("子线程正在计算");
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            sum += i;
        }
        return sum;
    }
}

在这段代码中可以看出,首先创建了一个实现了 Callable 接口的 Task,然后把这个 Task 实例传入到 FutureTask 的构造函数中去,创建了一个 FutureTask 实例,并且把这个实例当作一个 Runnable 放到 new Thread() 中去执行,最后再用 FutureTask 的 get 得到结果,并打印出来。

执行结果是 4950,正是任务里 0+1+2+...+99 的结果。

总结

今天关于Future就讲到这里,大家应该都已经get到了,感谢你能阅读到这里,希望你收获满满。我们下期见。

欢迎大家点赞,关注。我会持续输出更多技术干货!

弦外之音

学习一点点,努力一点点,分享一点点,大家加油!