前言

随着多核处理器的出现,如何轻松高效的进行异步编程变得愈发重要,我们看看在java8之前,使用java语言完成异步编程有哪些方案。

JAVA8之前的异步编程

  • 继承Thead类,重写run方法
  • 实现runable接口,实现run方法
  • 匿名内部类编写thread或者实现runable的类,当然在java8中可以用lambda表达式简化
  • 使用futureTask进行附带返回值的异步编程
  • 使用线程池和Future来实现异步编程
  • spring框架下的@async获得异步编程支持

使用线程池与future来实现异步编程

实现方式可谓是多种多样,这里我们使用线程池和future来实现异步编程,借着这个例子来讲述java8的组合式异步编程有着怎样的优势

        //构造线程池
ExecutorService pool = Executors.newCachedThreadPool();
try {
//构造future结果,doSomethingA十分耗时,因此采用异步
Future<Integer> future = pool.submit(() -> doSomethingA());
//做一些别的事情
doSomethingB();
//从future中获得结果,设置超时时间,超过了就抛异常
Integer result = future.get(10, TimeUnit.SECONDS);
//打印结果
System.out.printf("the async result is : %d", result);
//异常捕获
} catch (InterruptedException e) {
System.out.println("任务计算抛出了一个异常!");
} catch (ExecutionException e) {
System.out.println("线程在等待的过程中被中断了!");
} catch (TimeoutException e) {
System.out.println("future对象等待时间超时了!");
}
}

然而这样的异步编程方式仅仅能满足基本的需要,稍微复杂的一些异步处理Future接口似乎就有点束手无策了,例如

  • 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第

    一个的结果。
  • 等待 Future 集合中的所有任务都完成。
  • 仅等待 Future 集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同

    一个值) ,并返回它的结果。
  • 通过编程方式完成一个 Future 任务的执行(即以手工设定异步操作结果的方式) 。
  • 应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)

这种感觉其实就很像没有stream之前的collections的操作感觉一样,同样的,对于future,java8提供了它的函数式升级版本CompletableFuture,从名字就可以看出来这绝对是future的升级版。

JAVA8中的组合式异步编程

使用CompletableFuture进行异步编程

事物的发展往往都是由简单->复杂->简单,这里我们同样遵循这样的规律,循序渐进。

下面的例子摘取《java8实战》的异步编程章节,并做了简化。

我们假设现在我们有一项查询商品价格的服务十分耗时,所以毫无例外的我们想让查询最佳价格的服务以异步的形式执行。

最直接的方式是直接构建一个异步查询商品价格的api,并且返回,为了演示需要,编写一个线程等待一秒的方法来模拟长时间的请求。


public double getPrice(String product) {
return calculatePrice(product);
} private double calculatePrice(String product) {
delay();
return random.nextDouble() * product.charAt(0) + product.charAt(1);
} public static void delay() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

现在getPrice这方法是一个同步的方法,该方法在经过1秒的延迟之后会返回给我们一个商品的价格(这里只是简单的根据名字构造了一个随机数)

我们使用completFuture将getPrice转化为异步方法,如下

    public Future<Double> getAsyncPrice(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
double price = calculatePrice(product);
futurePrice.complete(price);
}).start();;
return futurePrice;
}

这里构造一个completableFuture对象,并另起一个异步线程,将异步计算的结果使用futurePrice.complete来接受,无需等待直接返回future结果

调用类使用Integer result = future.get(10, TimeUnit.SECONDS)来接受返回的结果,如果等待超时则抛出异常。

另外,如果异步线程发生异常,并且在排查问题的时候想要知道具体是什么原因导致的,

可以在getAsyncPrice方法中使用completeExcepitonally来得到异常信息并且结束这次异步任务,代码如下

public Future<Double> getPriceAsync(String product) {
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread(() -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}
}).start();
return futurePrice;
}

这样,基本的功能就实现了。

使用工厂类简化异步操作

也许你看到上面的代码,会说:"我晕,你这写法比原来还复杂哦,而且我也没看出啥区别啊。",是的,上文的写法可以算是原生态的写法了,目的为为下面的知识做一个简单的铺垫。

事实上,CompleteFuture本身提供了大量的工厂方法来供我们十分方便的实现一个异步编程,他封装了前篇一律的异常与结果接收,你只需要编写真正的异步逻辑部分就可以了,同时借住于lambda表达式,可以更进一步。

supplyAsync 方法接受一个生产者(Supplier)作为参数,返回一个 CompletableFuture

对象, 该对象完成异步执行后会读取调用生产者方法的返回值。 生产者方法会交由 ForkJoinPool

池中的某个执行线程(Executor)运行,但是你也可以使用 supplyAsync 方法的重载版本,传

递第二个参数指定不同的执行线程执行生产者方法。

于是上文的例子可以改写如下

    public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}

是不是简洁了许多呢?

可现在还有问题,这里我们成功的编写了一个十分简洁的异步方法,可实际的情况中,我们所能调用的API大部分都是同步的,因此下面将介绍如何使用异步的方法去操作这些同步API。

使用流异步操作同步API

我们现在有这么一个需求,它接受产品名作为参数,返回一个字符串列表,

这个字符串列表中包括商店的名称、该商店中指定商品的价格,商店集合以及接口设计如下。

List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll")); public List<String> findPrices(String product);

使用同步的方法实现

这样的集合变换使用stream流来操作十分容易,代码如下

    public List<String> findPrices(String product) {
return shops.stream()
.map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
.collect(toList());
}

stream流将 shop映射为了shop的名称以及该shop中商品的价格的字符串,并使用收集器进行收集。

使用异步的方法实现

事实上,我们完全可以使用流将shop映射成CompletableFuture对象,就好像在操作集合一样,代码如下

    List<CompletableFuture<String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture
.supplyAsync(() -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))))
.collect(toList());

使用这种方式,你会得到一个,List<CompletableFuture>,列表中的每个CompletableFuture 对象在计算完成后都包含商店的String类型的名称。但是,由于你用CompletableFutures 实现的findPrices方法要求返回一个List,你需要等待所有的future 执行完毕,将其包含的值抽取出来,填充到列表中才能返回。

为了实现这个效果,你可以向最初的 List<CompletableFuture> 施加第二个map 操作,对 List 中的所有future对象执行join操作,一个接一个地等待它们运行结束。注意CompletableFuture类中的join方法和Future接口中的get有相同的含义,并且也声明在Future 接口中,它们唯一的不同是join不会抛出任何检测到的异常。使用它你不再需要使用try / catch 语句块让你传递给第二个map方法的Lambda表达式变得过于臃肿。所有这些整合在一起,你就可以重新实现 findPrices 了,具体代码如下

    public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures = shops.stream()
.map(
shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is " + shop.getPrice(product)))
.collect(Collectors.toList()); return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}

以上的代码你可能会疑惑,为什么不直接按照shop->completableFuture->join->collect

的方式进行流处理呢?那是因为join这一步本身是阻塞的,对于流操作来说,前一个shop没有处理完之前,是不会处理下一个shop的,所以对于每一个shop,处理到join这一步的时候就会阻塞住等待1秒,这样的话,这个流水线本身就会变回阻塞的了。

而上文的编写方法可以看出 shop->completableFuture->collect 这个操作本身是非阻塞的,顺利的将所有的请求都发出去了,随后再使用join来完成结果的收集。

使用线程池来管控异步方法

前面在介绍工厂方法时提到,可以选择第二参数放入一个线程池来进行管控。

   private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});

接着在supplyAsync中使用该线程池即可,代码如下

CompletableFuture.supplyAsync(() -> shop.getName() + " price is " +
shop.getPrice(product), executor);

进阶的异步流操作

既然我们已经将异步操作与流相结合了,因此很容易的就会想到对于异步流来说,应该有会有类似于集合流的一些非常好用的API吧?事实上,JAVA8的确为我们提供了这些API。

构造同步和异步操作

如同集合流操作一样,异步流也可以提前安排一系列的任务,然后让异步任务有条不紊的按照这个顺序去执行。

  • 同步任务

    使用future.thenApply(Function)来实现,该方法接受一个Function对象

    你可以规划这样的任务 任务A(异步)->任务B(同步),语法可能是这样的
    stream()
.map(xxx->supplayAsync(()->任务A)) //这一步已经异步的映射成了任务A
.map(future->future.thenApply(任务B)//执行同步的任务B
.collect
  • 异步任务

    与同步几乎一样,方法变为future.thenCompose(Function)

    你可以规划这样的任务 任务A(异步)->任务B(同步)->任务C(异步),语法可能是这样的
    stream()
.map(xxx->supplayAsync(()->任务A)) //这一步已经异步的映射成了任务A
.map(future->future.thenApply(任务B)//执行同步的任务B
.map(future->future.thenCompose(任务C))//再异步执行任务C
.collect

将两个 CompletableFuture 对象整合起来,无论它们是否存在依赖

使用thenCombine来完成,类似任务A与任务B,A是查询价格,B是查询汇率,这两个任务之间本身没有关联关系,所以可以同时发起,但你最后需要计算价格乘以汇率,因此在这两个任务完成之后需要对他们的结果进行合并,代码如下

        Future<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product))//任务A
.thenCombine(
CompletableFuture.supplyAsync(() -> exchangeService.getRate(Money.EUR, Money.USD)), //任务B
(price, rate) -> price * rate); //任务A与任务B的合并操作

注意这里的任务A与任务B是异步的,但他们的合并操作是同步的,如果想要合并操作也是异步的,使用future.thenCombineAsync的异步方法版本。

对结果进行处理

使用thenAccept(Consumer)

以上都是对结果进行一些映射,你现在要对结果只进行处理,说白了就是前面的都是Function,现在要换成consumer,并且参数不再是异步任务,而是任务的结果值,举个例子,上文的任务A(异步)->任务B(同步)->任务C(异步) 的操作现在想到对他们的操作结果进行打印

就可以使用thenAccept(Consumer)

    stream()
.map(xxx->supplayAsync(()->任务A)) //这一步已经异步的映射成了任务A
.map(future->future.thenApply(任务B)//执行同步的任务B
.map(future->future.thenCompose(任务C))//再异步执行任务C
.map(future->future.thenAccept(System.out::println))//将结果打印
.collect

使用allOf与anyOf对结果进行处理

需要注意的是在执行了Accpet方法之后,你得到的是一个 CompletableFuture流对象

你可以对这些流对象进行类似及早求值的操作,例如这条查询4个商家的价格服务只要有一个给出了返回结果就结束这次异步流。

CompletableFuture[] futures = findPricesStream("myPhone")
.map(f -> f.thenAccept(System.out::println))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.anyOf(futures).join();

allOf 工厂方法接收一个由 CompletableFuture 构成的数组,数组中的所有 Completable-Future 对象执行完成之后,它返回一个 CompletableFuture 对象。这意味着,如果你需要等待最初 Stream 中的所有 CompletableFuture 对象执行完毕,对 allOf 方法返回的CompletableFuture 执行 join 操作是个不错的主意。这个方法对“最佳价格查询器”应用也是有用的,因为你的用户可能会困惑是否后面还有一些价格没有返回,使用这个方法,你可以在执行完毕之后打印输出一条消息“All shops returned results or timed out” 。然而在另一些场景中,你可能希望只要 CompletableFuture 对象数组中有任何一个执行完毕就不再等待,比如,你正在查询两个汇率服务器,任何一个返回了结果都能满足你的需求。在这种情况下,你可以使用一个类似的工厂方法 anyOf 。该方法接收一个 CompletableFuture 对象构成的数组, 返回由第一个执行完毕的 CompletableFuture 对象的返回值构成的 Completable-Future

总结

本文是对Java8实战中异步编程章节的一些整理和汇总,介绍了利用新增的completableFuture将异步任务与流操作集合起来实现组合式异步编程,利用工厂方法与函数接口可以大大的简化代码,同时提高代码的可阅读性,想要查看详细,可以自行翻阅该书。

Java8函数之旅 (八) - 组合式异步编程的更多相关文章

  1. java8的版本对组合式异步编程

    讨论了Java 8中的函数式数据处理,它可以将对集合数据的多个操作以流水线的方式组合在一起.本节继续讨论Java 8的新功能,主要是一个新的类CompletableFuture,它是对65节到83节介 ...

  2. Java编程的逻辑 (94) - 组合式异步编程

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  3. 如何设计一门语言(八)——异步编程和CPS变换

    关于这个话题,其实在(六)里面已经讨论了一半了.学过Haskell的都知道,这个世界上很多东西都可以用monad和comonad来把一些复杂的代码给抽象成简单的.一看就懂的形式.他们的区别,就像用js ...

  4. Java8函数之旅 (七) - 函数式备忘录模式优化递归

    前言 在上一篇开始Java8之旅(六) -- 使用lambda实现Java的尾递归中,我们利用了函数的懒加载机制实现了栈帧的复用,成功的实现了Java版本的尾递归,然而尾递归的使用有一个重要的条件就是 ...

  5. Java8函数之旅 (一) 开始认识lambda

    系列之前我想说的   最近有一段时间没写博客了,这几天回到学校,才闲下来,决定写一写最近学习到的知识,既是为了分享,也是为了巩固.之前看到过一篇调查,调查说的是学习新知识,光只是看的话,知识的获取率只 ...

  6. Java8函数之旅 (二) --Java8中的流

    流与集合    众所周知,日常开发与操作中涉及到集合的操作相当频繁,而java中对于集合的操作又是相当麻烦.这里你可能就有疑问了,我感觉平常开发的时候操作集合时不麻烦呀?那下面我们从一个例子说起. 计 ...

  7. Java8函数之旅 (三) --几道关于流的练习题

    为什么要有练习题?    所谓学而不思则罔,思而不学则殆,在系列第一篇就表明我认为写博客,既是分享,也是自己的巩固,我深信"纸上得来终觉浅,绝知此事要躬行"的道理,因此之后的几篇博 ...

  8. 《Java 8 in Action》Chapter 11:CompletableFuture:组合式异步编程

    某个网站的数据来自Facebook.Twitter和Google,这就需要网站与互联网上的多个Web服务通信.可是,你并不希望因为等待某些服务的响应,阻塞应用程序的运行,浪费数十亿宝贵的CPU时钟周期 ...

  9. Java8函数之旅(四) --四大函数接口

    前言   Java8中函数接口有很多,大概有几十个吧,具体究竟是多少我也数不清,所以一开始看的时候感觉一脸懵逼,不过其实根本没那么复杂,毕竟不应该也没必要把一个东西设计的很复杂. 几个单词   在学习 ...

随机推荐

  1. Java学习笔记17---方法的重载与重写

    重载是指,一个类中定义了一个成员方法后,通过修改参数个数.参数类型或参数顺序,重新实现该方法,则这两个方法互为对方的重载方法. 重写是指,子类重新实现父类的成员方法. 重载后的方法,与原方法相比: ( ...

  2. 提高运维效率(二)桌面显示IP

    运维人员远控电脑询问IP时,总要告诉用户找ip的步骤,岂不很烦? 以下方法直观地把ip地址显示在桌面上,再做个入职培训,即可提高运维效率. 1.  下载bginfo.exe软件,放到域控下的netlo ...

  3. Java爬虫框架WebMagic——入门(爬取列表类网站文章)

    初学爬虫,WebMagic作为一个Java开发的爬虫框架很容易上手,下面就通过一个简单的小例子来看一下. WebMagic框架简介 WebMagic框架包含四个组件,PageProcessor.Sch ...

  4. 《Linux命令行与shell脚本编程大全》第二十章 正则表达式

    20.1 什么是正则表达式 20.1.1 定义 正则表达式是你所定义的模式模板.linux工具可以用它来过滤文本. 正则表达式利用通配符来描述数据流中第一个或多个字符. 正则表达式模式含有文本或特殊字 ...

  5. 使用Bitbucket Pipeline进行.Net Core项目的自动构建、测试和部署

    1. 引言 首先,Bitbucket提供支持Mercurial和Git版本控制系统的网络托管服务.简单来说,它类似于GitHub,不同之处在于它支持个人免费创建私有项目仓库.除此之外,Bitbucke ...

  6. Linux Redis集群搭建与集群客户端实现

    硬件环境 本文适用的硬件环境如下 Linux版本:CentOS release 6.7 (Final) Redis版本: Redis已经成功安装,安装路径为/home/idata/yangfan/lo ...

  7. PHP生成 uuid

    // 生成UUID,并去掉分割符 function guid() { if (function_exists('com_create_guid')){ $uuid = com_create_guid( ...

  8. ArcGIS jsAPI (4.x)本地部署字体符号乱码

    在下载了新版arcigs 的 JS API 后,每次部署在IIS中都会出现部件字体乱码的问题,需配置响应标头和添加文件映射 一. HTTP响应标头配置 在 IIS 中的 HTTP响应标头 中加入以下配 ...

  9. Netty4 学习笔记之四: Netty HTTP服务的实现

    前言 目前主流的JAVA web 的HTTP服务主要是 springMVC和Struts2,更早的有JSP/servlet. 在学习Netty的时候,发现Netty 也可以作HTTP服务,于是便将此整 ...

  10. .NET Core快速入门教程 2、我的第一个.NET Core App(Windows篇)

    一.前言 本篇开发环境?1.操作系统: Windows 10 X642.SDK: .NET Core 2.0 Preview 二.安装 .NET Core SDK 1.下载 .NET Core下载地址 ...