vlambda博客
学习文章列表

笑了,面试官问我知不知道异步编程的Future。

这是why的第 60 篇原创文章


荒腔走板


家好,我是 why,欢迎来到我连续周更优质原创文章的第 60 篇。


前两周写的我眼中的成都那篇文章还在持续发酵,全网阅读初步估计应该是过 100w 了。诚惶诚恐,但是需要说明一下的是其实我是一个技术博主来的,偶尔荒腔走板的写点生活相关的,所以这篇还是回到技术上。(虽然从后台数据来看,大家更喜欢看生活类的文章。


老规矩,先来一个简短的荒腔走板,给冰冷的技术文注入一丝色彩。

上面这图是我五年前,在学校宿舍拍的。

前几天由于有点事情,打开了多年没有打开的 QQ。然后突然推送了一个“那年今日”发送的动态。

这张图片就是那个动态里面的。

2015 年 8 月的时候正是大三放暑假的时间,但是那个暑假我找了一个实习,所以暑假期间住在学校里面。宿舍就我一个人。那个时候我完全没有意识到,这是我程序猿生涯的一个真正的开端,也是我学生时代提前结束的宣告。

8 月 5 日凌晨,一只小猫突然蹿到了宿舍里面,在宿舍里面旁若无人的,像宿管阿姨一样审查着一切东西。甚至直接跳到桌子上,看着我敲代码。完全不怕我的样子。

于是我把它放到了我的自行车上,当模特拍了几张照片。

初见这只小猫时的那种惊喜我还记忆犹新,但是这波回忆杀给我的更大的冲击是:原来,这件事已经过去五年了。

如果没有 QQ 的这个提醒,你让我想这件事是发生在什么时候的,我的第一反应肯定是好多年前的事情了吧,慢慢咂摸之后有可能才想起,原来是大三暑假的时候的事情,然后再仔细一算,原来是仅仅五年前的事情呀。

短短的五年怎么就发生了怎么多事情啊?把我这五年塞的满满当当的。

不知道为什么如果把人生求学、步入社会的各个阶段分开来看,我每跨过一个阶段,再次回头望的时候都感觉这好像是别人的故事啊。

幸好我自己一年年的记录了下来,幸好这真的是我自己的故事。

好了,说回文章。

你就是写了个假异步


这是 rpc 的四种调用方式:

笑了,面试官问我知不知道异步编程的Future。

文本主要分享这个 future 的调用方式,不讲 Dubbo 框架,这里只是一个引子。

谈到 future 的时候大家都会想到异步编程。但是你仔细看框起来这里:

笑了,面试官问我知不知道异步编程的Future。

客户端线程调用 future.get() 方法的时候还是会阻塞当前线程的。

我倒是觉得这充其量只能算一个阉割版的异步编程。

笑了,面试官问我知不知道异步编程的Future。

本文将带你从阉割版的 future 聊到升级版的 Google Guava 的 future,最后谈谈加强版的 future 。

先聊聊线程池的提交方式


到 Future 的时候,我们基本上就会想到线程池,想到它的几种提交方式。

先是最简单的,execute 方式提交,不关心返回值的,直接往线程池里面扔任务就完事:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        //execute(Runnable command)方法。没有返回值
        executor.execute(() -> {
            System.out.println("关注why技术");
        });
        Thread.currentThread().join();
    }
}


可以看一下 execute 方法,接受一个 Runnable 方法,返回类型是 void:

笑了,面试官问我知不知道异步编程的Future。

然后是 submit 方法。你知道线程池有几种 submit 方法吗?

虽然你经常用,但是可能你从来没有关心过人家。呸,渣男:

笑了,面试官问我知不知道异步编程的Future。

有三种 submit。这三种按照提交任务的类型来算分为两个类型。

  • 提交执行 Runnable 类型的任务。

  • 提交执行 Callable 类型的任务。

但是返回值都是 Future,这才是我们关心的东西。

也许你知道线程池有三种 submit 方法,但是也许你根本不知道里面的任务分为两种类型,你就只知道往线程池里面扔,也不管扔的是什么类型的任务。

笑了,面试官问我知不知道异步编程的Future。

我们先看一下 Callable 类型的任务是怎么执行的:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        Future<String> future = executor.submit(() -> {
            System.out.println("关注why技术");
            return "这次一定!";
        });
        System.out.println("future的内容:" + future.get());
        Thread.currentThread().join();
    }
}


这里利用 lambda 表达式,直接在任务体里面带上一个返回值,这时你看调用的方法就变成了这个:

笑了,面试官问我知不知道异步编程的Future。

运行结果也能拿到任务体里面的返回了。输出结果如下:

笑了,面试官问我知不知道异步编程的Future。

好,接下来再说说 submit 的任务为 Runable 类型的情况。

这个时候有两个重载的形式:

笑了,面试官问我知不知道异步编程的Future。

标号为 ① 的方法扔进去一个 Runable 的任务,返回一个 Future,而这个返回的 Future ,相当于是返回了一个寂寞。下面我会说到原因。

标号为 ② 的方法扔进去一个 Runable 的任务的同时,再扔进去一个泛型 T ,而巧好返回的 Future 里面的泛型也是 T,那么我们大胆的猜测一下这就是同一个对象。如果是同一个对象,说明我们可以一个对象传到任务体里面去一顿操作,然后通过 Future 再次拿到这个对象的。一会就去验证。

来,先验证标号为 ① 的方法,我为啥说它返回了一个寂寞。

首先,还是先把测试案例放在这里:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        Future<?> future = executor.submit(() -> {
            System.out.println("关注why技术");
        });
        System.out.println("future的内容:" + future.get());
        Thread.currentThread().join();
    }
}


可以看到,确实是调用的标号为 ① 的方法:

笑了,面试官问我知不知道异步编程的Future。

同时,我们也可以看到 future.get() 方法的返回值为 null。

你说,这不是返回了一个寂寞是干啥?

当你想用标号为 ① 的方法时,我劝你直接用 execute 方式提交任务。还不需要构建一个寂寞的返回值,徒增无用对象。

接下来,我们看看标号为 ② 的方法是怎么用的:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
        AtomicInteger atomicInteger = new AtomicInteger();
        Future<AtomicInteger> future = executor.submit(() -> {
            System.out.println("关注why技术");
            //在这里进行计算逻辑
            atomicInteger.set(5201314);
        }, atomicInteger);

        System.out.println("future的内容:" + future.get());
        Thread.currentThread().join();
    }
}


可以看到改造之后,确实是调用了标号为 ② 的方法:

笑了,面试官问我知不知道异步编程的Future。

future.get() 方法的输出值也是异步任务中我们经过计算后得出的 5201314。

你看,渣男就是这样,明明不懂你,还非得用甜言蜜语来轰炸你。呸。

笑了,面试官问我知不知道异步编程的Future。

好了。综上,线程池的提交方式一共有四种:一种 execute,无返回值。三种 submit,有返回值。

submit 中按照提交任务的类型又分为两种:一个是 Callable,一个是 Runable。

submit 中 Runable 的任务类型又有两个重载方法:一个返回了个寂寞,一个返回了个渣男。哦,不。一个返回了个寂寞,一个返回了个对象。

这个时候就有人要站出来说:你说的不对,你就是瞎说,明明就只有 execute 这一种提交方式。

是的,“只有 execute 这一种提交方式”这一种说法也是没错的。

请看源码:

笑了,面试官问我知不知道异步编程的Future。

三种 submit 方法里面调用的都是 execute 方法。

能把前面这些方法娓娓道来,从表面谈到内在的这种人,才是好人。

笑了,面试官问我知不知道异步编程的Future。

只有爱你,才会把你研究透。

当然,还有这几种提交方式,用的不多,就不展开说了:

笑了,面试官问我知不知道异步编程的Future。

写到这里我不禁想起了我的第三篇文章,真是奇怪的时间线开始收缩了的感觉,,这篇文章里面聊到了不同提交方式,对于异常的不同处理方式。

我就问你:一个线程池中的线程异常了,那么线程池会怎么处理这个线程?

你要是不知道,可以去看看这篇文章,毕竟,有可能在面试的时候遇到的:

笑了,面试官问我知不知道异步编程的Future。

好,上面这些东西捋清楚了之后。我们再聚焦到返回值 Future 上:

从上面的代码我们可以看出,当我们想要返回值的时候,都需要调用下面的这个 get() 方法:

笑了,面试官问我知不知道异步编程的Future。

而从这个方法的描述可以看出,这是一个阻塞方法。拿不到值就在那里等着。当然,还有一个带超时时间的 get 方法,等指定时间后就不等了。

呸,渣男。没耐心,这点时间都舍不得等。

笑了,面试官问我知不知道异步编程的Future。

总之就是有可能要等的。只要等,那么就是阻塞。只要是阻塞,就是一个假异步。

所以总结一下这种场景下返回的 Future 的不足之处:

  • 只有主动调用 get 方法去获取值,但是有可能值还没准备好,就阻塞等待。

  • 任务处理过程中出现异常会把异常隐藏,封装到 Future 里面去,只有调用 get 方法的时候才知道异常了。

写到这里的时候我不禁想起一个形象的例子,我给你举一个。

假设你想约你的女神一起去吃饭。女神嘛,肯定是要先画个美美的妆才会出去逛街的。而女神化妆就可以类比为我们提交的一个异步任务。

然后你就收拾行头准备出发,这就是你提交异步任务后还可以做一些自己的事情。

女神说:你先等着吧,我的妆还没画好呢。

于是你开始等待,无尽的等待。这就是不带超时时间的 future.get() 方法。

笑了,面试官问我知不知道异步编程的Future。

也有可能你硬气一点,对女神说:我最多再等 24 小时哈,超过 24 小时不下楼,我就走了。

这就是带超时时间的 future.get(timeout,unit) 方法:

笑了,面试官问我知不知道异步编程的Future。

结果 24 小时之后,女神还没下来,你就走了。

笑了,面试官问我知不知道异步编程的Future。
笑了,面试官问我知不知道异步编程的Future。

而真正的异步是你不用等我,我好了我就叫你。

这让我想起了好莱坞原则:Don't Call Us,We'll Call you!

接下来,让我们见识一下真正的异步。

什么叫真正的:“你先玩自己的,我一会好了叫你。”

笑了,面试官问我知不知道异步编程的Future。

Guava 的 Future


女神说的:“好了叫你”。

就是一种回调机制。说到回调,那么我们就需要在异步任务提交之后,注册一个回调函数就行。

Google 提供的 Guava 包里面对 JDK 的 Future 进行了扩展:

笑了,面试官问我知不知道异步编程的Future。

新增了一个 addListenter 方法,入参是一个 Runnable 的任务类型和一个线程池。

使用方法,先看代码:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
        ListenableFuture<String> listenableFuture = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()+"-女神:我开始化妆了,好了我叫你。");
            TimeUnit.SECONDS.sleep(5);
            return "化妆完毕了。";
        });

        listenableFuture.addListener(() -> {
            try {
                System.out.println(Thread.currentThread().getName()+"-future的内容:" + listenableFuture.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, executor);
        System.out.println(Thread.currentThread().getName()+"-等女神化妆的时候可以干点自己的事情。");
        Thread.currentThread().join();
    }
}


首先创建线程池的方式变了,需要用 Guava 里面的 MoreExecutors 方法装饰一下:


ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());


然后用装饰后的 executor 调用 submit 方法(任意一种),就会返回 ListenableFuture ,拿到这个 ListenableFuture 之后,我们就可以在上面注册监听:

笑了,面试官问我知不知道异步编程的Future。

所以,上面的程序我们调用的是入参为 callable 类型的接口:

笑了,面试官问我知不知道异步编程的Future。

从运行结果可以看出来:获取运行结果是在另外的线程里面执行的,完全没有阻塞主线程。

和之前的“假异步”还是有很大区别的。

除了上面的 addListener 方法外,其实我更喜欢用 FutureCallback 的方式。

笑了,面试官问我知不知道异步编程的Future。

可以看一下代码,非常的直观:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
        ListenableFuture<String> listenableFuture = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName()+"-女神:我开始化妆了,好了我叫你。");
            TimeUnit.SECONDS.sleep(5);
            return "化妆完毕了。";
        });
        Futures.addCallback(listenableFuture, new FutureCallback<String>() {
            @Override
            public void onSuccess(@Nullable String result)
{
                System.out.println(Thread.currentThread().getName()+"-future的内容:" + result);
            }

            @Override
            public void onFailure(Throwable t)
{
                System.out.println(Thread.currentThread().getName()+"-女神放你鸽子了。");
                t.printStackTrace();
            }
        });
        System.out.println(Thread.currentThread().getName()+"-等女神化妆的时候可以干点自己的事情。");
        Thread.currentThread().join();
    }
}


有 onSuccess 方法和 onFailure 方法。

上面的程序输出结果为:

笑了,面试官问我知不知道异步编程的Future。

如果异步任务执行的时候抛出了异常,比如女神被她的男神约走了,异步任务改成这样:

ListenableFuture<String> listenableFuture = executor.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "-女神:我开始化妆了,好了我叫你。");
            TimeUnit.SECONDS.sleep(5);
            throw new Exception("男神约我看电影,就不和你吃饭了。");
        });


最终的运行结果就是这样:

笑了,面试官问我知不知道异步编程的Future。

是的,女神去看电影了。她一定只是不想吃饭而已。

笑了,面试官问我知不知道异步编程的Future。

加强版的Future - CompletableFuture


第一小节讲的 Future 是 JDK 1.5 时代的产物:

笑了,面试官问我知不知道异步编程的Future。

经过了这么多年的发展,Doug Lea 在 JDK 1.8 里面引入了新的 CompletableFuture :

笑了,面试官问我知不知道异步编程的Future。

到了 JDK 1.8 时代,这才是真正的异步编程。

CompletableFuture 实现了两个接口,一个是我们熟悉的 Future ,一个是 CompletionStage。

CompletionStage接口,你看这个接口的名称中有一个 Stage :

笑了,面试官问我知不知道异步编程的Future。

可以把这个接口理解为一个任务的某个阶段。所以多个 CompletionStage 链接在一起就是一个任务链。前一个任务完成后,下一个任务就会自动触发。

CompletableFuture 里面的方法非常的多。

由于篇幅原因,我就只演示一个方法:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "-女神:我开始化妆了,好了我叫你。");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "化妆完毕了。";
        });

        completableFuture.whenComplete((returnStr, exception) -> {
            if (exception == null) {
                System.out.println(Thread.currentThread().getName() + returnStr);
            } else {
                System.out.println(Thread.currentThread().getName() + "女神放你鸽子了。");
                exception.printStackTrace();
            }
        });
        System.out.println(Thread.currentThread().getName() + "-等女神化妆的时候可以干点自己的事情。");
        Thread.currentThread().join();
    }
}


该方法的执行结果如下:

笑了,面试官问我知不知道异步编程的Future。

我们执行的时候并没有指定用什么线程池,但是从结果可以看到也是异步的执行。

从输出日志中是可以看出端倪的,ForkJoinPool.commonPool() 是其默认使用的线程池。

笑了,面试官问我知不知道异步编程的Future。

当然,我们也可以自己指定。

笑了,面试官问我知不知道异步编程的Future。

这个方法在很多开源框架里面使用的还是非常的多的。

接下来主要看看 CompletableFuture 对于异常的处理。我觉得非常的优雅。

不需要 try-catch 代码块包裹,也不需要调用 Future.get() 才知道异常了,它提供了一个 handle 方法,可以处理上游异步任务中出现的异常:

public class JDKThreadPoolExecutorTest {

    public static void main(String[] args) throws Exception {
        CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "-女神:我开始化妆了,好了我叫你。");
            throw new RuntimeException("男神约我看电影了,我们下次再约吧,你是个好人。");
        }).handleAsync((result, exception) -> {
            if (exception != null) {
                System.out.println(Thread.currentThread().getName() + "-女神放你鸽子了!");
                return exception.getCause();
            } else {
                return result;
            }
        }).thenApplyAsync((returnStr) -> {
            System.out.println(Thread.currentThread().getName() + "-" + returnStr);
            return returnStr;
        });
        System.out.println(Thread.currentThread().getName() + "-等女神化妆的时候可以干点自己的事情。");
        Thread.currentThread().join();
    }
}


所以,上面程序的输出结果如下:

笑了,面试官问我知不知道异步编程的Future。

如果,你顺利把女神约出来了,是这样的:

笑了,面试官问我知不知道异步编程的Future。

好了,女神都约出来了,文章就到这里了。去干正事吧。

最后说一句(求关注)


按照我的经验,女神约出来了你需要准备好回答一个问题:

你看我今天有什么不同?

首先这题就是一道送命题,回答到她预期的答案的概率非常的低。有可能她今天不一样的地方就是换了一个指甲油、换了一个美瞳、换了一个耳环之类的。

很明显,这些非常细节的地方我们很难发现。但是别怂。

先含情脉脉的认真的盯着她,花一分钟找答案,一分钟后没有找到答案,就说:

你每天都不一样,每天都比昨天更加美丽。


好了,都看到这里了安排个“一键三连”(转发、在看、点赞)吧,周更很累的,不要白嫖我,需要一点正反馈。


你根本想象不到,这周我是在怎样的挤时间,才把这篇文章给怼出来。感谢红牛和咖啡的加持。


笑了,面试官问我知不知道异步编程的Future。

才疏学浅,难免会有纰漏,如果你发现了错误的地方,由于本号没有留言功能,还请你在后台留言指出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

笑了,面试官问我知不知道异步编程的Future。

我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

还有,重要的事情说三遍: 

欢迎关注我呀。 

欢迎关注我呀。 

欢迎关注我呀。

往期推荐





转发、点赞、在看、一键三连。

别白嫖我,好吗?