需求
有一家商店提供的价格是以欧元(EUR)计价的,但是你希望以美元的方式提供给你的客户。你可以用异步的方式向商店查询指定商品的价格,同时从远程的汇率服务那里查到欧元和美元之间的汇率。当二者都结束时,再将这两个结果结合起来,用返回的商品价格乘以当时的汇率,得到以美元计价的商品价格。
实现源码
获取价格
import com.practisesvr.utils.DelayUtils;
import lombok.Data;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import static com.zhangziwa.practisesvr.utils.DelayUtils.getMoment;
@Data
public class Shop {
private final String name;
private final Random random;
public Shop(String name) {
this.name = name;
random = new Random(name.charAt(0) * name.charAt(1) * name.charAt(2));
}
public double getPrice(String product) {
return calculatePrice(product);
}
private double calculatePrice(String product) {
DelayUtils.delay();
System.out.println(getMoment() + " " + Thread.currentThread().getName() + " 线程执行 calculatePrice");
// 依据产品的名称,生成一个随机值作为价格
return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
}
汇率换算
import com.zhangziwa.practisesvr.utils.DelayUtils;
// 汇率换算服务
public class ExchangeService {
public enum Money {
USD(1.0), EUR(1.35387), GBP(1.69715), CAD(.92106), MXN(.07683);
private final double rate;
Money(double rate) {
this.rate = rate;
}
}
public static double getRate(Money source, Money destination) {
return getRateWithDelay(source, destination);
}
private static double getRateWithDelay(Money source, Money destination) {
DelayUtils.delay();
return destination.rate / source.rate;
}
}
实现方案
方案1:thenCombine
→ future结果map加工
public List<String> findPricesInUSD(String product) {
List<CompletableFuture<Double>> priceFutures = new ArrayList<>();
for (Shop shop : shops) {
CompletableFuture<Double> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor)
.thenCombine(CompletableFuture.supplyAsync(() -> ExchangeService.getRate(Money.EUR, Money.USD), executor), (price, rate) -> price * rate);
priceFutures.add(futurePriceInUSD);
}
List<String> prices = priceFutures.stream().map(CompletableFuture::join).map(price -> " price is " + price).collect(Collectors.toList());
return prices;
}
方案2:thenCombine
+thenApply
+list.add
public List<String> findPricesInUSD2(String product) {
List<CompletableFuture<String>> priceFutures = new ArrayList<>();
for (Shop shop : shops) {
CompletableFuture<String> futurePriceInUSD = CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor)
.thenCombine(CompletableFuture.supplyAsync(() -> ExchangeService.getRate(Money.EUR, Money.USD), executor), (price, rate) -> price * rate)
.thenApply(price -> shop.getName() + " price is " + price);
priceFutures.add(futurePriceInUSD);
}
List<String> prices = priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
return prices;
}
方案3:thenCombine
+thenApply
+toList
(自定义线程池)
private final Executor executor = Executors.newFixedThreadPool(shops.size(), ExecuterThreadFactoryBuilder.build());
public List<String> findPricesInUSD4(String product) {
List<CompletableFuture<String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product),executor)
.thenCombine(CompletableFuture.supplyAsync(() -> ExchangeService.getRate(Money.EUR, Money.USD), executor), (price, rate) -> price * rate)
.thenApply(price -> shop.getName() + " price is " + price))
.collect(Collectors.toList());
List<String> prices = priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
return prices;
}
2024-04-21 09:14:11 thread-2 执行calculatePrice LetsSaveBig
2024-04-21 09:14:11 thread-1 getRate BestPrice
2024-04-21 09:14:11 thread-0 执行calculatePrice BestPrice
2024-04-21 09:14:11 thread-2 getRate LetsSaveBig
2024-04-21 09:14:11 thread-0 执行calculatePrice ShopEasy
2024-04-21 09:14:11 thread-0 getRate ShopEasy
2024-04-21 09:14:12 thread-2 thenCombine打折 LetsSaveBig
2024-04-21 09:14:12 thread-1 thenCombine打折 BestPrice
2024-04-21 09:14:12 thread-0 thenCombine打折 ShopEasy
2024-04-21 09:14:12 thread-1 thenApply BestPrice
2024-04-21 09:14:12 thread-2 thenApply LetsSaveBig
2024-04-21 09:14:12 thread-0 thenApply ShopEasy
2024-04-21 09:14:11 main [BestPrice price is 91.040141702717, LetsSaveBig price is 125.1710573102377, ShopEasy price is 130.0591978746988]
性能比较 total cost time : 1064 ms
combined USD thenCombine+thenApply+toList+supplyAsync cost : 1064 ms, 100.00%
方案4:thenCombine
+thenApply
+toList
(默认线程池)
public List<String> findPricesInUSD3(String product) {
List<CompletableFuture<String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product))
.thenCombine(CompletableFuture.supplyAsync(() -> ExchangeService.getRate(Money.EUR, Money.USD)), (price, rate) -> price * rate)
.thenApply(price -> shop.getName() + " price is " + price))
.collect(Collectors.toList());
List<String> prices = priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
return prices;
}
2024-04-21 09:16:06 ForkJoinPool.commonPool-worker-3 执行calculatePrice LetsSaveBig
2024-04-21 09:16:06 ForkJoinPool.commonPool-worker-2 getRate BestPrice
2024-04-21 09:16:06 ForkJoinPool.commonPool-worker-4 getRate LetsSaveBig
2024-04-21 09:16:06 ForkJoinPool.commonPool-worker-1 执行calculatePrice BestPrice
2024-04-21 09:16:06 ForkJoinPool.commonPool-worker-6 getRate ShopEasy
2024-04-21 09:16:06 ForkJoinPool.commonPool-worker-5 执行calculatePrice ShopEasy
2024-04-21 09:16:07 ForkJoinPool.commonPool-worker-2 thenCombine打折 BestPrice
2024-04-21 09:16:07 ForkJoinPool.commonPool-worker-6 thenCombine打折 ShopEasy
2024-04-21 09:16:07 ForkJoinPool.commonPool-worker-4 thenCombine打折 LetsSaveBig
2024-04-21 09:16:07 ForkJoinPool.commonPool-worker-6 thenApply ShopEasy
2024-04-21 09:16:07 ForkJoinPool.commonPool-worker-2 thenApply BestPrice
2024-04-21 09:16:07 ForkJoinPool.commonPool-worker-4 thenApply LetsSaveBig
2024-04-21 09:16:06 main [BestPrice price is 91.040141702717, LetsSaveBig price is 125.1710573102377, ShopEasy price is 130.0591978746988]
性能比较 total cost time : 1058 ms
combined USD thenCombine+thenApply+toList+supplyAsync cost : 1058 ms, 100.00%
方案5:Java7写法
// 为了更直观地感受一下使用CompletableFuture在代码可读性上带来的巨大提升,尝试仅使用Java 7中提供的特性实现以下如上实现
public List<String> findPricesInUSDJava7(String product) {
ExecutorService executor = Executors.newCachedThreadPool();
List<Future<Double>> priceFutures = new ArrayList<>();
for (Shop shop : shops) {
final Future<Double> futureRate = executor.submit(new Callable<Double>() {
public Double call() {
return ExchangeService.getRate(Money.EUR, Money.USD);
}
});
Future<Double> futurePriceInUSD = executor.submit(new Callable<Double>() {
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);
}
List<String> prices = new ArrayList<>();
for (Future<Double> priceFuture : priceFutures) {
try {
prices.add(" price is " + priceFuture.get());
}
catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
return prices;
}
2024-04-09 21:59:37 pool-2-thread-10线程 执行calculatePrice
2024-04-09 21:59:37 pool-2-thread-4线程 执行calculatePrice
2024-04-09 21:59:37 pool-2-thread-2线程 执行calculatePrice
2024-04-09 21:59:37 pool-2-thread-8线程 执行calculatePrice
2024-04-09 21:59:37 pool-2-thread-6线程 执行calculatePrice
2024-04-09 21:59:36 main [ price is 91.040141702717, price is 125.1710573102377, price is 158.16078708139523, price is 136.45612204497712, price is 130.0591978746988]
性能比较 total cost time : 1056 ms
combined USD findPricesInUSDJava7 cost : 1056 ms, 100.00%
我们能看到创建流水线对同步和异步操作进行混合操作有多么简单,随着处理任务和需要合并结果数目的增加,这种声明式程序设计的优势也愈发明显。
测试
import com.zhangziwa.practisesvr.utils.StopWatchUtils;
import org.springframework.util.StopWatch;
import java.util.List;
import java.util.function.Supplier;
import static com.zhangziwa.practisesvr.utils.DelayUtils.getMoment;
public class BestPriceFinderMain {
private static BestPriceFinder bestPriceFinder = new BestPriceFinder();
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch("性能比较");
execute("combined USD CompletableFuture v1", () -> bestPriceFinder.findPricesInUSD("myPhone27S"), stopWatch);
execute("combined USD CompletableFuture v2", () -> bestPriceFinder.findPricesInUSD2("myPhone27S"), stopWatch);
execute("combined USD CompletableFuture v4", () -> bestPriceFinder.findPricesInUSD4("myPhone27S"), stopWatch);
execute("combined USD CompletableFuture v3", () -> bestPriceFinder.findPricesInUSD3("myPhone27S"), stopWatch);
execute("combined USD findPricesInUSDJava7", () -> bestPriceFinder.findPricesInUSDJava7("myPhone27S"), stopWatch);
StopWatchUtils.logStopWatch(stopWatch);
}
private static void execute(String msg, Supplier<List<String>> s, StopWatch stopWatch) {
stopWatch.start(msg);
System.out.println(getMoment() + " " + Thread.currentThread().getName() + " " + s.get());
stopWatch.stop();
System.out.println();
}
}
性能比较 total cost time : 8123 ms
combined USD thenCombine cost : 2063 ms, 25.40%
combined USD thenCombine+thenApply cost : 2010 ms, 24.74%
combined USD thenCombine+thenApply+toList+supplyAsyncExecutor cost : 2013 ms, 24.78%
combined USD thenCombine+thenApply+toList+supplyAsync cost : 1018 ms, 12.53%
combined USD Java7 cost : 1017 ms, 12.52%
参考
《Java8 实战》第11章 CompletableFuture:组合式异步编程