2024年Java最全《Java8实战》笔记(11):CompletableFuture-组合式异步编程,2024华为Java高级面试题及答案

最后

即使是面试跳槽,那也是一个学习的过程。只有全面的复习,才能让我们更好的充实自己,武装自己,为自己的面试之路不再坎坷!今天就给大家分享一个Github上全面的Java面试题大全,就是这份面试大全助我拿下大厂Offer,月薪提至30K!

我也是第一时间分享出来给大家,希望可以帮助大家都能去往自己心仪的大厂!为金三银四做准备!
一共有20个知识点专题,分别是:

Dubbo面试专题

JVM面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Java并发面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Kafka面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

MongDB面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

MyBatis面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

MySQL面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Netty面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

RabbitMQ面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Redis面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

Spring Cloud面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

SpringBoot面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

zookeeper面试专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

常见面试算法题汇总专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

计算机网络基础专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

设计模式专题

这个GItHub上的Java项目开源了,2020最全的Java架构面试复习指南

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

在getPrice方法中引入一个模拟的延迟

public double getPrice(String product) {

return calculatePrice(product);

}

private double calculatePrice(String product) {

delay();

return random.nextDouble() * product.charAt(0) + product.charAt(1);

}

将同步方法转换为异步方法

将getPrice转换为getPriceAsync方法,并修改它的返回值:

public Future getPriceAsync(String product) {

CompletableFuture futurePrice = new CompletableFuture<>();

new Thread( () -> {

double price = calculatePrice(product);

futurePrice.complete(price);

}).start();

//无需等待还没结束的计算,直接返回Future对象

return futurePrice;

}

使用异步API

ShopMain

运行结果

Invocation returned after 43 msecs

Price is 123.26

Price returned after 1045 msecs

错误处理

如果价格计算过程中产生了错误会怎样呢?非常不幸,这种情况下你会得到一个相当糟糕的结果:用于提示错误的异常会被限制在试图计算商品价格的当前线程的范围内,最终会杀死该线程,而这会导致等待get方法返回结果的客户端永久地被阻塞。

客户端可以使用重载版本的get方法,它使用一个超时参数来避免发生这样的情况。使用这种方法至少能防止程序永久地等待下去,超时发生时,程序会得到通知发生了TimeoutException。

为了让客户端能了解商店无法提供请求商品价格的原因,你需要使用CompletableFuture的completeExceptionally方法将导致CompletableFuture内发生问题的异常抛出。

public Future getPriceAsync(String product) {

CompletableFuture futurePrice = new CompletableFuture<>();

new Thread( () -> {

try {

double price = calculatePrice(product);

futurePrice.complete(price);

} catch (Exception ex) {

futurePrice.completeExceptionally(ex);

}

}).start();

return futurePrice;

}

使用工厂方法supplyAsync创建CompletableFuture

CompletableFuture类自身提供了大量精巧的工厂方法,使用这些方法能更容易地完成整个流程,还不用担心实现的细节。

public Future getPriceAsync(String product) {

return CompletableFuture.supplyAsync(() -> calculatePrice(product));

}

让你的代码免受阻塞之苦——将无法改变的同步适配成异步


BestPriceFinder

BestPriceFinderMain

你已经被要求进行“最佳价格查询器”应用的开发了,不过你需要查询的所有商店只提供了同步API。

换句话说,你有一个商家的列表,如下所示:

List shops = Arrays.asList(new Shop(“BestPrice”),

new Shop(“LetsSaveBig”),

new Shop(“MyFavoriteShop”),

new Shop(“BuyItAll”));

采用顺序查询所有商店的方式实现的findPrices方法

public List findPrices(String product) {

return shops.stream()

.map(shop -> String.format(“%s price is %.2f”,

shop.getName(), shop.getPrice(product)))

.collect(toList());

}

验证findPrices的正确性和执行性能

long start = System.nanoTime();

System.out.println(findPrices(“myPhone27S”));

long duration = (System.nanoTime() - start) / 1_000_000;

System.out.println(“Done in " + duration + " msecs”);

运行结果:

[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price

is 214.13, BuyItAll price is 184.74]

Done in 4032 msecs

使用并行流对请求进行并行操作

public List findPrices(String product) {

return shops.parallelStream()

.map(shop -> String.format(“%s price is %.2f”,

shop.getName(), shop.getPrice(product)))

.collect(toList());

}

使用CompletableFuture发起异步请求

public List findPrices(String product) {

List<CompletableFuture> 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());

}

寻找更好的方案

execute(“sequential”, () -> bestPriceFinder.findPricesSequential(“myPhone27S”));

execute(“parallel”, () -> bestPriceFinder.findPricesParallel(“myPhone27S”));

execute(“composed CompletableFuture”, () -> bestPriceFinder.findPricesFuture(“myPhone27S”));

execute(“composed CompletableFuture2”, () -> bestPriceFinder.findPricesFuture2(“myPhone27S”));

运行结果

[BestPrice price is 123.25651664705744, LetsSaveBig price is 169.4653393606115, MyFavoriteShop price is 214.12914480588853, BuyItAll price is 184.74384995303313]

sequential done in 4076 msecs

[BestPrice price is 197.15388829450728, LetsSaveBig price is 167.59404755738808, MyFavoriteShop price is 192.48730292081552, BuyItAll price is 199.67823140124116]

parallel done in 2013 msecs

[BestPrice price is 171.10524235618578, LetsSaveBig price is 168.59369176671822, MyFavoriteShop price is 174.79155890558252, BuyItAll price is 154.82955565763797]

composed CompletableFuture done in 1011 msecs

[BestPrice price is 227.53480147033423, LetsSaveBig price is 200.89398407500244, MyFavoriteShop price is 161.14747297059597, BuyItAll price is 155.9041805933185]

composed CompletableFuture2 done in 1005 msecs

它们看起来不相伯仲,究其原因都一样:它们内部采用的是同样的通用线程池,默认都使用固定数目的线程,具体线程数取决于Runtime.getRuntime().availableProcessors()的返回值。然而,CompletableFuture具有一定的优势,因为它允许你对执行器(Executor)进行配置,尤其是线程池的大小,让它以更适合应用需求的方式进行配置,满足程序的要求,而这是并行流API无法提供的

使用定制的执行器

创建一个配有线程池的执行器,线程池中线程的数目取决于你预计你的应用需要处理的负荷,但是你该如何选择合适的线程数目呢


调整线程池的大小

《Java并发编程实战》一书中,Brian Goetz和合著者们为线程池大小的优化提供了不少中肯的建议。这非常重要,如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。Brian Goetz建议,线程池大小与处理器的利用率之比可以使用下面的公式进行估算:

N-threads = N-CPU * U-CPU * (1 + W/C)

其中:

  • N-CPU是处理器的核的数目,可以通过Runtime.getRuntime().availableProcessors()得到

  • U-CPU是期望的CPU利用率(该值应该介于0和1之间)

  • W/C是等待时间与计算时间的比率


你的应用99%的时间都在等待商店的响应,所以估算出的W/C比率为1/100。这意味着如果你期望的CPU利用率是100%,你需要创建一个拥有400个线程的线程池。

实际操作中,如果你创建的线程数比商店的数目更多,反而是一种浪费,因为这样做之后,你线程池中的有些线程根本没有机会被使用。出于这种考虑,我们建议你将执行器使用的线程数,与你需要查询的商店数目设定为同一个值,这样每个商店都应该对应一个服务线程。

不过,为了避免发生由于商店的数目过多导致服务器超负荷而崩溃,你还是需要设置一个上限,比如100个线程.

private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100),new ThreadFactory() {

public Thread newThread(Runnable r) {

Thread t = new Thread®;

t.setDaemon(true);

return t;

}

});

注意,你现在正创建的是一个由守护线程构成的线程池。Java程序无法终止或者退出一个正在运行中的线程,所以最后剩下的那个线程会由于一直等待无法发生的事件而引发问题。

与此相反,如果将线程标记为守护进程,意味着程序退出时它也会被回收。这二者之间没有性能上的差异。现在,你可以将执行器作为第二个参数传递给supplyAsync工厂方法了。

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

处理需大量使用异步操作的情况时,利用CompletableFutures向其提交任务执行几乎是最有效的策略。

并行——使用Stream还是CompletableFutures?

目前为止,你已经知道对集合进行并行计算有两种方式:

  1. 要么将其转化为并行流,利用map这样的操作开展工作,

  2. 要么枚举出集合中的每一个元素,创建新的线程,在CompletableFuture内对其进行操作。

后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助你确保整体的计算不会因为线程都在等待I/O而发生阻塞。

我们对使用这些API的建议如下。

  • 如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程)。

  • 反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用CompletableFuture灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者W/C的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。

对多个异步任务进行流水线操作


BestPriceFinder2

BestPriceFinderMain2

让我们假设所有的商店都同意使用一个集中式的折扣服务。

Discount

实现折扣服务

Quote

使用Discount服务

由于Discount服务是一种远程服务,你还需要增加1秒钟的模拟延迟。

以最简单的、顺序的而且同步执行的实现。

public List findPricesSequential(String product) {

return shops.stream()

.map(shop -> shop.getPrice2(product))

.map(Quote::parse)

.map(Discount::applyDiscount)

.collect(Collectors.toList());

}

  • 第一个操作将每个shop对象转换成了一个字符串,该字符串包含了该 shop中指定商品的价格和折扣代码。

  • 第二个操作对这些字符串进行了解析,在Quote对象中对它们进行转换。

  • 最终,第三个map会操作联系远程的Discount服务,计算出最终的折扣价格,并返回该价格及提供该价格商品的shop。

[BestPrice price is 110.93, LetsSaveBig price is 135.58, MyFavoriteShop price is 192.72, BuyItAll price is 184.74, ShopEasy price is 167.28]

sequential done in 10238 msecs

这种实现方式的性能远非最优,只是运行基准测试。

把流转换为并行流的方式,非常容易提升该程序的性能。

Stream底层依赖的是线程数量固定的通用线程池,扩展性差。相反,你也知道,如果自定义CompletableFutures调度任务执行的执行器能够更充分地利用CPU资源

构造同步和异步操作

List findPrices(String product) {

List<CompletableFuture> priceFutures = shops.stream()

.map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor))

.map(future -> future.thenApply(Quote::parse))

.map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)))

.collect(toList());

return priceFutures.stream()

.map(CompletableFuture::join)

.collect(toList());

}

三次转换流程图

运行结果

[BestPrice price is 204.78, LetsSaveBig price is 190.85, MyFavoriteShop price is 128.92, BuyItAll price is 140.31, ShopEasy price is 166.1]

composed CompletableFuture done in 2025 msecs

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

CompletableFuture futurePriceInUSD =

CompletableFuture.supplyAsync(() -> shop.getPrice(product))

.thenCombine(

CompletableFuture.supplyAsync(

() -> ExchangeService.getRate(Money.EUR, Money.USD)),

(price, rate) -> price * rate

);

对Future和CompletableFuture的回顾

前两节非常清晰地呈现了相对于采用Java 8 之前提供的Future 实现, CompletableFuture 版本实现所具备的巨大优势

CompletableFuture利用Lambda表达式以声明式的API提供了一种机制,能够用最有效的方式,非常容易地将多个以同步或异步方式执行复杂操作的任务结合到一起。

为了更直观地感受一下使用CompletableFuture在代码可读性上带来的巨大提升,尝试仅使用Java 7中提供的特性。

利用Java 7的方法合并两个Future对象,实现上一节例子。

ExecutorService executor = Executors.newCachedThreadPool();

List<Future> priceFutures = new ArrayList<>();

for (Shop shop : shops) {

final Future futureRate = executor.submit(new Callable() {

public Double call() {

return ExchangeService.getRate(Money.EUR, Money.USD);

}

});

Future futurePriceInUSD = executor.submit(new Callable() {

public Double call() {

try {

double priceInEUR = shop.getPrice(product);

return priceInEUR * futureRate.get();

} catch (InterruptedException | ExecutionException e) {

throw new RuntimeException(e.getMessage(), e);

}

}

});

priceFutures.add(futurePriceInUSD);

}

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

ngeService.getRate(Money.EUR, Money.USD);

}

});

Future futurePriceInUSD = executor.submit(new Callable() {

public Double call() {

try {

double priceInEUR = shop.getPrice(product);

return priceInEUR * futureRate.get();

} catch (InterruptedException | ExecutionException e) {

throw new RuntimeException(e.getMessage(), e);

}

}

});

priceFutures.add(futurePriceInUSD);

}

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值