在多任务程序中,我们比较熟悉的是分支-合并框架的并行计算,他的目的是将一个操作(比如巨大的List计算)切分为多个子操作,充分利用CPU的多核,甚至多个机器集群,并行执行这些子操作。
而CompletableFuture的目标是并发(执行多个操作),而非并行,是利用CPU的核,使其持续忙碌,达成最大吞吐,在并发进行中避免等待远程服务的返回值,或者数据库的长时查询结果等耗时较长的操作,如果解决了这些问题,就能获得最大的并发(通过避免阻塞)。
而分支-合并框架只是并行计算,是没有阻塞的情况。
Future接口
Future接口用于对将来某个时刻发生的结果进行建模,它建模了一种异步计算,返回一个执行结果的引用,计算完成后,这个引用被返回给调用方,
在使用Future时,将耗时操作封装在一个Callable接口对象中,提交给ExecutorService。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
我们在异步执行结果获取时,设置了get过期时间2秒,否则,如果没有正常的返回值,会阻塞线程,非常危险。
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public class FutureDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
Util.delay();
return new Random().nextInt();
}
});
doSomeThingElse();
executor.shutdown();
try {
try {
System.out.println("result:" + result.get(2,TimeUnit.SECONDS));
} catch (TimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private static void doSomeThingElse() {
System.out.println("Do Some Thing Else." );
}
}
因为Future的局限性,如
- 难异步合并
- 等待 Future 集合中的所有任务都完成。
- 仅等待 Future 集合中最快结束的任务完成,并返回它的结果。
一是我们没有好的方法去获取一个完成的任务;二是 Future.get 是阻塞方法,使用不当会造成线程的浪费。解决第一个问题可以用 CompletionService 解决,CompletionService 提供了一个 take() 阻塞方法,用以依次获取所有已完成的任务。对于第二个问题,可以用 Google Guava 库所提供的 ListeningExecutorService 和 ListenableFuture 来解决。这些都会在后面的介绍。
CompletableFuture组合异步
单一操作异步
我们的演示程序是查询多个在线商店,把最佳的价格返回给消费者。首先先查询一个产品的价格(即单一操作异步)。
先建一个同步模型商店类Shop,其中delay()是一个延时阻塞。
calculatePrice(String)同步输出一个随机价格,
getPriceAsync(String)是异步获取随机价格,依赖calculatePrice同步方法,返回类型是Future<Double>
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
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) {
delay();
return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread( () -> {
double price = calculatePrice(product);
futurePrice.complete(price);
}).start();
return futurePrice;
}
public String getName() {
return name;
}
}
delay()阻塞,延迟一秒,注释的代码是随机延迟0.5 - 2.5秒之间
public static void delay() {
int delay = 1000;
//int delay = 500 + RANDOM.nextInt(2000);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
调用单一查询同步:
public class ShopSyncMain {
public static void main(String[] args) {
Shop shop = new Shop("BestShop");
long start = System.nanoTime();
double price = shop.getPrice("my favorite product");
System.out.printf("Price is %.2f%n", price);
long invocationTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Invocation returned after " + invocationTime
+ " msecs");
// Do some more tasks
doSomethingElse();
long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Price returned after " + retrievalTime + " msecs");
}
private static void doSomethingElse() {
System.out.println("Doing something else...");
}
}
//结果:
Price is 123.26
Invocation returned after 1069 msecs
Doing something else...
Price returned after 1069 msecs
嗯,一秒出头,这符合delay()阻塞一秒的预期
调用单一查询异步操作:
public class ShopMain {
public static void main(String[] args) {
Shop shop = new Shop("BestShop");
long start = System.nanoTime();
Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
long invocationTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Invocation returned after " + invocationTime
+ " msecs");
// Do some more tasks, like querying other shops
doSomethingElse();
// while the price of the product is being calculated
try {
double price = futurePrice.get();
System.out.printf("Price is %.2f%n", price);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
System.out.println("Price returned after " + retrievalTime + " msecs");
}
private static void doSomethingElse() {
System.out.println("Doing something else...");
}
}
结果:
Invocation returned after 54 msecs
Doing something else...
Price is 123.26
Price returned after 1102 msecs
咦,异步反而时间多了,别忘了,大家都是执行一个方法,不存在多个并发(也没有多个顺序流操作),异步当然显示不出优势。
异步异常
new Thread方式新建线程可能会有个糟糕的情况:用于提示错误的异常被限制在当前线程内,最终会杀死线程,所以get是无法返回期望值,client调用方会被阻塞。
你当然可以重载get,要求超时过期时间,可以解决问题,但你也应该解决这个错误逻辑,避免触发超时,因为超时又会触发TimeoutException,client永远不知道为啥产生了异常。
为了让client获知异常原因,需要对CompletableFuture对象使用completeExceptionally方法,将内部错误异常跑出。
改写如下:
public Future<Double> getPrice(String product) {
new Thread(() -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}
}).start();
使用工厂方法创建CompletableFuture对象
前面用门通过手动建立线程,通过调用同步方法创建CompletableFuture对象,还可以通过CompletableFuture的工厂方法来方便的创建对象,重写如下:
public Future<Double> getPrice(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
通过原生代码,我们可以了解supplyAsync和runAsync的区别,runAsync没有返回值,只是运行一个Runnable对象,supplyAsync有返回值U,参数是Supplier函数接口,或者可以自定义一个指定的执行线程池。
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
Executor executor) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}
public static CompletableFuture<Void> runAsync(Runnable runnable,
Executor executor) {
return asyncRunStage(screenExecutor(executor), runnable);
}
并发单一任务异步
前面是单一的操作异步,异步的内涵不能显现,现在要查询一大堆的商店(并发)的同一产品的价格,然后对比价格。
商店List:
private final List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll")/*,
new Shop("ShopEasy")*/);
使用三种方式处理多个商店的价格查询:
//顺序同步方式
public List<String> findPricesSequential(String product) {
return shops.stream()
.map(shop -> shop.getName() + " price is " + shop.getPrice(product))
.collect(Collectors.toList());
}
//并行流方式
public List<String> findPricesParallel(String product) {
return shops.parallelStream()
.map(shop -> shop.getName() + " price is " + shop.getPrice(product))
.collect(Collectors.toList());
}
//工厂方法异步
public List<String> findPricesFuture(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is "
+ shop.getPrice(product), executor))
.collect(Collectors.toList());
List<String> prices = priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
return prices;
}
private final Executor executor = Executors.newFixedThreadPool(shops.size(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
CompletableFuture::join的作用是异步的开启任务并发,并且将结果合并聚集。
结果:
sequential done in 4130 msecs
parallel done in 1091 msecs
composed CompletableFuture done in 1010 msecs
如果将list个数变成5个,结果是
同步必然是线性增长
sequential done in 5032 msecs
并行流受到PC内核数的限制(4个),所以一组并行只能指派最多4个线程,第二轮要多费时1秒多
parallel done in 2009 msecs
CompletableFuture异步方式能保持1秒的秘密在于线程池,我们定义了shops.size()大小的线程池,并且使用了守护线程。java提供了俩类的线程:用户线程和守护线程(user thread and Daemon thread)。用户线程是高优先级的线程。JVM虚拟机在结束一个用户线程之前,会先等待该用户线程完成它的task。
在另一方面,守护线程是低优先级的线程,它的作用仅仅是为用户线程提供服务。正是由于守护线程是为用户线程提供服务的,仅仅在用户线程处于运行状态时才需要守护线程。另外,一旦所有的用户线程都运行完毕,那么守护线程是无法阻止JVM退出的
并发多任务异步
假设所有商店开始折扣服务,用5种折扣码代表。
public class Discount {
public enum Code {
NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20);
private final int percentage;
Code(int percentage) {
this.percentage = percentage;
}
}
public static String applyDiscount(Quote quote) {
return quote.getShopName() + " price is " + Discount.apply(quote.getPrice(), quote.getDiscountCode());
}
private static double apply(double price, Code code) {
delay();
return format(price * (100 - code.percentage) / 100);
}
}
public class Quote {
private final String shopName;
private final double price;
private final Discount.Code discountCode;
public Quote(String shopName, double price, Discount.Code discountCode) {
this.shopName = shopName;
this.price = price;
this.discountCode = discountCode;
}
public static Quote parse(String s) {
String[] split = s.split(":");
String shopName = split[0];
double price = Double.parseDouble(split[1]);
Discount.Code discountCode = Discount.Code.valueOf(split[2]);
return new Quote(shopName, price, discountCode);
}
public String getShopName() {
return shopName;
}
public double getPrice() {
return price;
}
public Discount.Code getDiscountCode() {
return discountCode;
}
}
比如,SILVER(5)代表95折, GOLD(10)代表90折,PLATINUM(15)代表85折。、
Quote类用于包装输出。同步的价格计算也做了修改,使用:来分割商店名、价格、折扣码。折扣码是随机产生的。
新的商店类,getPrice同步方法输出商店名、价格、折扣码的待分割字符串。parse用于将字符串转化为Quote实例。
打折延迟1秒。
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 String getPrice(String product) {
double price = calculatePrice(product);
Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
return name + ":" + price + ":" + code;
}
public double calculatePrice(String product) {
delay();
return format(random.nextDouble() * product.charAt(0) + product.charAt(1));
}
public String getName() {
return name;
}
}
运行三种服务
public class BestPriceFinder {
private final List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("ShopEasy"));
private final Executor executor = Executors.newFixedThreadPool(shops.size(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
//同步流
public List<String> findPricesSequential(String product) {
return shops.stream()
.map(shop -> shop.getPrice(product))
.map(Quote::parse)
.map(Discount::applyDiscount)
.collect(Collectors.toList());
}
//并行流
public List<String> findPricesParallel(String product) {
return shops.parallelStream()
.map(shop -> shop.getPrice(product))
.map(Quote::parse)
.map(Discount::applyDiscount)
.collect(Collectors.toList());
}
//CompletableFuture异步
public List<String> findPricesFuture(String product) {
List<CompletableFuture<String>> priceFutures = findPricesStream(product)
.collect(Collectors.<CompletableFuture<String>>toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
public Stream<CompletableFuture<String>> findPricesStream(String product) {
return 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)));
}
public void printPricesStream(String product) {
long start = System.nanoTime();
CompletableFuture[] futures = findPricesStream(product)
.map(f -> f.thenAccept(s -> System.out.println(s + " (done in " + ((System.nanoTime() - start) / 1_000_000) + " msecs)")))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();
System.out.println("All shops have now responded in " + ((System.nanoTime() - start) / 1_000_000) + " msecs");
}
}
//结果
sequential done in 10176 msecs
parallel done in 4010 msecs
composed CompletableFuture done in 2016 msecs
- 同步的10多秒是在5个list元素时获得的,每个一次询价,一次打折延迟,共2秒。
- 并行流一轮需要2秒多(第一轮询价4个+1个共两个轮次,2秒,第二轮打折4个+1个)。
- 异步方式第一轮和第二轮都是1秒多,所以2秒多。
这里的三步需要特别说明,这也是并发多任务的核心所在,并发是5条记录并发,多任务是
- 询价任务(耗时1秒)
- 解析字符换成Quote实例
- 计算折扣
这三个子任务
- .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)));
第一行是用异步工厂执行同步方法getPrice,返回字符串,涉及线程池;
第二行thenApply是将字符串转成Quote实例,这个是同步内存处理,不涉及线程池;
thenapply()是接受一个Function<? super T,? extends U> fn参数用来转换CompletableFuture,相当于流的map操作,返回的是非CompletableFuture类型,它的功能相当于将CompletableFuture<T>转换成CompletableFuture<U>.
在这里是把CompletableFuture<String>转成CompletableFuture<Quote>
第三行也是异步处理,也涉及线程池,获得折扣后的价格。thenCompose()在异步操作完成的时候对异步操作的结果进行一些操作,Function<? super T, ? extends CompletionStage<U>> fn参数,并且仍然返回CompletableFuture类型,相当于flatMap,用来连接两个CompletableFuture
如果加入一个不相互依赖的Future对象进行整合,比如需要计算汇率(不需要计算折扣,他们之间无依赖),可以试用thenCombine方法,这里有4种实现,最后一种比较优。
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) {
delay();
return destination.rate / source.rate;
}
}
public List<String> findPricesInUSD(String product) {
List<CompletableFuture<Double>> priceFutures = new ArrayList<>();
for (Shop shop : shops) {
// Start of Listing 10.20.
// Only the type of futurePriceInUSD has been changed to
// CompletableFuture so that it is compatible with the
// CompletableFuture::join operation below.
CompletableFuture<Double> futurePriceInUSD =
CompletableFuture.supplyAsync(() -> shop.getPrice(product))
.thenCombine(
CompletableFuture.supplyAsync(
() -> ExchangeService.getRate(Money.EUR, Money.USD)),
(price, rate) -> price * rate
);
priceFutures.add(futurePriceInUSD);
}
// Drawback: The shop is not accessible anymore outside the loop,
// so the getName() call below has been commented out.
List<String> prices = priceFutures
.stream()
.map(CompletableFuture::join)
.map(price -> /*shop.getName() +*/ " price is " + price)
.collect(Collectors.toList());
return prices;
}
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(/*shop.getName() +*/ " price is " + priceFuture.get());
}
catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
return prices;
}
public List<String> findPricesInUSD2(String product) {
List<CompletableFuture<String>> priceFutures = new ArrayList<>();
for (Shop shop : shops) {
// Here, an extra operation has been added so that the shop name
// is retrieved within the loop. As a result, we now deal with
// CompletableFuture<String> instances.
CompletableFuture<String> futurePriceInUSD =
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);
priceFutures.add(futurePriceInUSD);
}
List<String> prices = priceFutures
.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
return prices;
}
public List<String> findPricesInUSD3(String product) {
// Here, the for loop has been replaced by a mapping function...
Stream<CompletableFuture<String>> priceFuturesStream = 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));
// However, we should gather the CompletableFutures into a List so that the asynchronous
// operations are triggered before being "joined."
List<CompletableFuture<String>> priceFutures = priceFuturesStream.collect(Collectors.toList());
List<String> prices = priceFutures
.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
return prices;
}
来源:oschina
链接:https://my.oschina.net/u/4313515/blog/4187058