千家信息网

如何理解异步编程的Future

发表于:2024-11-23 作者:千家信息网编辑
千家信息网最后更新 2024年11月23日,本篇内容介绍了"如何理解异步编程的Future"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!先聊聊线
千家信息网最后更新 2024年11月23日如何理解异步编程的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:

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

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

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

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

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

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

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

我们先看一下 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 future = executor.submit(() -> {             System.out.println("关注why技术");             return "这次一定!";         });         System.out.println("future的内容:" + future.get());         Thread.currentThread().join();     } }

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

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

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

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

标号为 ① 的方法扔进去一个 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.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 future = executor.submit(() -> {             System.out.println("关注why技术");             //在这里进行计算逻辑             atomicInteger.set(5201314);         }, atomicInteger);          System.out.println("future的内容:" + future.get());         Thread.currentThread().join();     } }

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

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

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

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

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

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

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

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

请看源码:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

假设你是一个小屌丝,那么女神就会对你说:我已经开始化妆了,你到楼下了就给我打电话。

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

你花了一小时到了女神楼下,打电话给她:女神你好,我到你楼下了。

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

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

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

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

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

当然,还有一种情况就是你到楼下给女神打电话,女神说:哎,今天我男神约我出去看电影,就不和你去吃饭了哈。本来我想提前给你说的,但是我又记不起你电话,只有你打过来我才能告诉你。就这样,你自己玩去吧。

这就相当于异步任务执行过程中抛出了异常,而你只有在调用了 get 方法(打电话操作)之后才知道原来异常了。

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

就像女神接到男神的电话时说的:我需要一点时间准备一下,你先玩自己的吧,我一会好了给你打电话。

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

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

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

Guava 的 Future

女神说的:"好了叫你"。

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

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

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

使用方法,先看代码:

public class JDKThreadPoolExecutorTest {      public static void main(String[] args) throws Exception {         ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());         ListenableFuture 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 之后,我们就可以在上面注册监听:

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

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

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

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

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

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

上面的程序输出结果为:

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

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

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

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

加强版的Future - CompletableFuture

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

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

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

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

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

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

CompletableFuture 里面的方法非常的多。

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

public class JDKThreadPoolExecutorTest {      public static void main(String[] args) throws Exception {         CompletableFuture 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();     } }

该方法的执行结果如下:

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

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

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

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

接下来主要看看 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"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

方法 任务 女神 化妆 就是 时候 类型 线程 方式 结果 内容 只有 标号 寂寞 对象 时间 面的 输出 接下来 事情 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 数据库技术与应用大纲 数据库服务器和基站区别 网络安全与执法毕业论文 电子邮件服务器未响应 数据库维护要求 mysql 数据库表设计 linux服务器操作日志 网络安全手抄报四年级a3纸 河湖划界数据库界桩类型 游戏服务器玩家少怎么解决 汽车服务器为什么延迟2秒 北京手机软件开发学校 什么是信创网络安全 大连信实海大网络技术有限公司 成都项目软件开发价格 有哪些公开数据库 无线网络技术科技有限公司 网络终端管理服务器品牌 中国交通标志识别数据库 cf埃及服连接不到服务器 常州企业软件开发费用是多少 贵州数据网络技术服务工程 北邮网络安全考研复试要机考吗 济宁学院计算机系网络安全专业 软长城汽车软件开发岗位面试 数据库技术 论文答辩问题 河北定制网络技术咨询信息推荐 智乐软件开发过什么 艾思网络技术有限公司 域名服务器和域名系统一样吗
0