JAVA8 - 异步编程

Future

Future 接口在JAVA5 中被引入,设计初衷式对将来某个时刻会发生的结果进行建模。它建模了一中异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future 中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作,不再需要呆呆等等待耗时的操作完成。

下面代码展示了JAVA 8 之前使用Future 的一个例子

ExecutorService executorService = Executors.newCachedThreadPool();
Future<Double> future = executorService.submit(new Callable<Double>() {
    public Double call() {
        return doSomeLongComputation();
    }
});

doSomethingElse(); //异步操作计算时可以作一些其他事情

try{
    Double aDouble = future.get(1, TimeUnit.SECONDS);
    System.out.println(aDouble);
}catch (ExecutionException ee){
    //计算抛出一个异常
}catch (InterruptedException ie){
    //当前线程等待被中断
}catch (TimeoutException te){
    //在Future 对象完成任务前已超时
}

使用Future 以异步方式执行长时间的操作:

Future 接口的局限性:

我们很难表述Future 结果之间的依赖性,从文字描述上这很简单,"当长时间计算任务完成时,请将计算的结果通知到另一个长时间运行的计算任务,这两个计算任务都完成后,将计算的结果与另外一个查询操作结果合并"。
但是,使用Future 中提供的方法完成这样的操作又是另外一回事。这也是我们需要更具描述能力的特性的原因,比如下面这些:

  • 将两个异步计算合并为一个--这两个异步计算之间互相独立,同时第二个又依赖于第一个的结果
  • 等待Future 集合中的所有任务都完成
  • 仅等待Future 集合中最快结束的任务完成(有可能因为它们试图通过不同的方式计算同一个值),并返回它的结果
  • 通过编程方式完成一个Future 任务执行(即以手工设定异步操作结果的方式)
  • 应对Future 的完成事件(即当Future 的完成事件发生时会收到通知,并能使用Future 计算的结果进行下一步的操作,不只是简单地当阻塞等待操作的结果)

使用ComletableFuture 构建异步应用

商店类:

package com.demo3;

import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;

public class Shop {

  private String name;

  public Shop(String name) {
      this.name = name;
  }

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

  //模拟1s延迟的方法
  public static void delay(){
      try {
          Thread.sleep(1000L);
      } catch (InterruptedException e) {
          throw new RuntimeException(e);
      }
  }

  private double calculatePrice(String product){
      delay();
      return new Random().nextDouble() * product.charAt(0) * product.charAt(1);
  }
}

测试类:

package com.demo3;

import org.junit.Test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class TestShop {


  @Test
  public void test1() {
      Shop shop = new Shop("BestShop");
      long start = System.nanoTime();
      Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
      long invocationTime = (System.nanoTime() - start) / 1000000;

      System.out.println("Invocation returned  after " + invocationTime + "msecs");
      //执行更所任务,比如查询其他商店
      doSomeThingElse();


      try {
          double price = futurePrice.get();
          System.out.printf("Price is %.2f%n", price);
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
      long retrievalTime = (System.nanoTime() - start) / 1000000;
      System.out.println("Price returned after " + retrievalTime + "msecs");
  }

  private void doSomeThingElse() {
      System.out.println("doSomeThingElse ===========");
  }
}

输出:

Invocation returned  after 2msecs
doSomeThingElse ===========
Price is 1891.60
Price returned after 1022msecs

你一定会发现getPriceAsync 方法的调用返回远远早于最终价格计算完成的时间,在后面你还会知道有可能避免发生客户端被阻塞的风险。实际上这非常简单,Future 执行完毕可以发送一个通知,仅在计算结果可用时执行一个 由 Lambda 表达式或者方法引用定义的回调函数。现在要解决的另一个问题是:如何正确地管理异步任务执行过程中可能出现的错误

错误处理

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

shop.getPriceAsync(null) 时:测试类一直收阻塞

客户端可以使用重载版本的get,使用着这种方法至少能防止程序永久地等待下去,超时发生时,客户端会得到通知发生了TimeoutExecption。不过,也因此,你不会有机会发现计算商品价格的线程内到底发生了什么问题才引发这样的失效,为了让客户端能了解商店无法提供请求商品价格的原因,你需使用CompletableFuture 的 completeExceptionally 方法 将导致 CompletableFuture 内发生问题的异常抛出

优化后的结果如下:

public Future<Double> getPriceAsync(String product){
    CompletableFuture<Double> future = new CompletableFuture<>();
    new Thread(() -> {
        try{
            double price = calculatePrice(product);
            future.complete(price);
        }catch (Exception ex){
            future.completeExceptionally(ex); //抛出导致失败的异常,完成这次Future操作
        }

    }).start();
    return future;
}

执行结果:测试类接收到future 抛出的异常

使用工厂方法supplyAsync创建CompletableFuture

supplyAsync 方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture 对象。生产者方法交由ForkJoinPool池中的某个执行线程(Executor)运行,但是你也可以使用该方法的重载版本,传递第二个参数指定不同的执行线程执行生产者方法。

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

该段代码与上面一段代码完全等价,这意味着它提供了同样得错误管理机制,而前者你花费了大量的精力才得以构建

让你的代码免受阻塞之苦

Shop类:

package com.java8;

import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;

public class Shop {

    private String name;

    public Shop(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }


    //模拟1s延迟的方法
    public static void delay(){
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private double calculatePrice(String product){
        delay(); //模拟计算延迟
        return new Random().nextDouble() * product.charAt(0) * product.charAt(1);
    }

    public double getPrice(String product){
        return calculatePrice(product);
    }
}

需求:有个商店列表, 查找某个商品在每个商店的价格

方法签名:

public List<String> findPrices(String product);

采用顺序查询方式实现

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

}

测试耗时:

@Test
public void testFindPrices(){
    long start  = System.nanoTime();
    System.out.println(findPrices("myPhone27S"));
    long duration = (System.nanoTime() - start) / 1000000;
    System.out.println("done in " + duration + "msecs");
}

输出:

[shop1 price is 2990.36, shop2 price is 5299.88, shop3 price is 12852.29, shop4 price is 1674.74]
done in 10msecs

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

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

}

测试结果:

[shop1 price is 32341.87, shop2 price is 68639.81, shop3 price is 39455.04, shop4 price is 84082.52]
done in 1016msecs

使用CompletableFuture 发起异步请求

List<CompletableFuture<String>> collect = shops.stream()
        .map((s) -> CompletableFuture.supplyAsync(  //对每个shop 创建异步任务
                () -> String.format("%s price is %.2f", s.getName(), s.getPrice(product))
        )).collect(Collectors.toList());

使用这种方式会得到一个List<CompletableFuture>,但是findPrices 要求的返回值是一个List,你需要等待所有到future 执行完毕,将其包含的值抽取出来,填充到列表才能返回

为了实现这个效果,可以向 List<CompletableFuture> 施加第二个map操作,对List 中的所有future 对象执行join 操作,一个饥饿一个地二代它们运行结束。CompletableFuture 类中的join方法和Future接口中的get 有相同的含义, 并也声明在Future 接口中,它们唯一的不同是join 不会抛出任何检测到的异常。整合在一起就实现了如下:

public List<String> findPrices(String product){
    List<CompletableFuture<String>> collect = shops.stream()
            .map((s) -> CompletableFuture.supplyAsync(  //对每个shop 创建异步任务
                    () -> String.format("%s price is %.2f", s.getName(), s.getPrice(product))
            )).collect(Collectors.toList());

    return collect.stream().
            map(CompletableFuture::join) //join 等价于 get ,join 不会抛出任何检测到的异常
            .collect(Collectors.toList());
}

测试结果:

[shop1 price is 90166.21, shop2 price is 56769.05, shop3 price is 15295.77, shop4 price is 5828.36]
done in 1025msecs

需要注意的是,这里使用了两个不同的 stream 流水线,而不是在同一个处理流的流水线上一个接一个地放置两个map操作--这是有缘由的。考虑流之间的延迟特性,如果你在单一流水线中处理流,发向不同的商家的请求只能以同步、顺序执行的方式才会成功。因此,每个创建CompletableFuture 对象只能在前一个操作结束之后执行查询指定商家的动作、通知join 方法返回计算结果,下图解释了这些重要的细节

上半部分展示了使用单一流水线处理流的过程,我们看到,执行的流程(以虚线标识)的是顺序的。

寻找更好的方案

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

使用定制的执行器

线程池中的线程的数据取决于你的应用需要处理的符合,但是你该如何选择合适的线程数目哪?

实际操中,如果你创建的线程数比商店的数目更多,反而是一种浪费,因为这样做之后,你的线程池中有些线程根本没有机会被使用。我们建议将执行器使用的线程数,与你需要查询的商店数目设定为同一个值,这样每个商店都应该对应一个服务线程。不过,为了避免发生由于商店的数目过多导致服务器超负荷而崩溃,你还是需要设置一个上限,比如 400 个线程。代码如下:

public List<String> findPrices(String product){
    Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    });

    List<CompletableFuture<String>> collect = shops.stream()
            .map((shop) -> CompletableFuture.supplyAsync(() -> shop.getName() + "price is" + shop.getPrice(product), executor))
            .collect(Collectors.toList());

    return collect.stream().map(CompletableFuture::join).collect(Collectors.toList());
}

测试结果:

[shop1 price is 23449.468712965067, shop2 price is 42535.523537359324, shop3 price is 27190.1655096438, shop4 price is 12467.32936640171]
done in 1013msecs

以上创建的是一个由守护线程构成的线程池。当一个非守护线程在执行时,JAVA 程序无法终止或者退出,所以最后剩下的那个线程会由于一直等待无法发生的事件而引发问题。与此相反,如果是守护进程,意味着程序退出时它也会被回收,这二者之间没有性能差异

newFixedThreadPool线程池:https://blog.csdn.net/ssyyjj88/article/details/78115985
守护线程和非守护线程:https://blog.csdn.net/Bananaaay/article/details/134271496

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

修改Shop 类下的 getPrice 方法,调用getPrice 方法可以得到一个像下面这样的返回值:
BestPrice: 123.26:GOLD

public String getPrice(String product){
    double price = calculatePrice(product);
    Discount.Code code =  Discount.Code.values()[new Random().nextInt(Discount.Code.values().length)];

    return String.format("%s:%.2f:%s", product, price, code );
}

实现折扣服务:

package com.java8;


/**
 * ShopName:price:Discount.Code 字符串封装类
 */
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;
    }
}

Discount 类模拟远程服务:

package com.java8;

import java.util.List;

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 price * (100 - code.percentage) / 100;
    }

    public static void delay(){
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

使Discount 服务:

public List<String> findPrices(String product) {
  List<String> collect = shops.stream().map(shop -> shop.getPrice(product))
          .map(Quote::parse) //封装为 Quote 对象
          .map(Discount::applyDiscount) // 调用远程服务,计算出最终的折扣价格并返回该价格及商品价格
          .collect(Collectors.toList());

  return collect;
}

输出结果:

[myPhone27S price is3379.51, myPhone27S price is11774.178, myPhone27S price is281.432, myPhone27S price is6143.5835]
done in 8071msecs

构造同步和异步操作

public List<String> findPrices(String product) {

    Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    });
    List<CompletableFuture<String>> collect = shops.stream().map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product)))
            .map(future -> future.thenApply(Quote::parse)) //封装到 quote
            .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)))
            .collect(Collectors.toList());


    return collect.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

测试结果:

[myPhone27S price is 4828.96, myPhone27S price is 462.61, myPhone27S price is 3177.419, myPhone27S price is 10403.73]
done in 2044msecs

1.获取价格

第一个转换的结果是一个Stream<CompletableFuture>,一旦运行结束,每个CompletabFuture 对象中都会包含对应shop 返回的字符串

2.解析报价

现在需要进行第二次转换将字符串转变为订单。由于一般情况下解析操作不设计任何远程服务,也不会进行任何I/O操作,它几乎可以在第一时间进行,所以能够采用同步操作,不会带来太多的延迟。由于整个原因,你可以对第一步中生成的CompletableFuture 对象调用它的 thenApply,将一个由字符串转换Quote 的方法作为参数从传递给他。

注意到了吗?知道调用的CompletableFuture 执行结束,使用的thenApply方法都不会阻塞你代码的运行。这意味着CompletableFuture 最终结束运行时,你希望传递Lambda 表达式给thenApply 方法,将stream 中的每个CompletableFuture 对象转换为对应的CompletableFuture 对象。你可以把整个看成是为处理CompletableFuture 的结果建立了一个菜单,就像你曾经为 stream 的流水线所做的事儿一样

3.为计算折扣价格构造Future

第三个map 操作涉及联系远程的Discount服务,这一转换与前一个转换又不大一样,因为这一次转换需要远程执行(就这个例子而言,它需要模拟远程调用带来的延迟),出于这一原因,你也希望它能够异步执行,你希望把它们以级联的方式串接起来进行工作。

  • 从shop对象中获取价格,接着把价格转换为Quote
  • 拿到返回的Quote对象,将其作为参数传递给Discount服务,取得最终的折扣价格

JAVA 8 的CompletableFuture API提供了名为 thenCompose 的方法,它就是专门为这一目的而设计的,thenCompose 方法允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作。 换句话说,你可以创建两个CompletableFuture对象,对第一个CompletableFuture 对象调用thenCompose, 并向其传递一个函数。当第一个CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一个CompletableFuture的返回做输入计算出的第二个CompletableFuture 对象。使用这种方式,即使Future 在向不同的商店收集报价,主线程还是能继续执行其他的重要操作。比如,响应UI事件

thenCompose 方法像CompletableFuture 类中的其他方法一样,也提供了一个以Async后缀结尾的版本thenComposeAsync。通常而言,名称中不带Async后缀结尾的方法和它的前一个任务一样,在同一个线程中运行;而名称以Async 结尾的方法将后续的任务提交到一个线程池,所以每个任务是由不同线程处理的。就这个例子而言,第二个CompletableFuture 对象的结果取决于第一个CompletableFuture,所以无论你使用哪个版本的方法来处理CompletableFuture对象,对于最终的结果,或者大致的时间而言都没有多少差别。我们选择thenCompose 方法的原因是因为它更高效一些,因为少了很多线程切换的开销

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

另外一种比较常见的情况是你需要将两个完全不相干的CompletableFuture 对象的结果整合起来,而且你也不希望等到第一个任务完结才开始第二个任务。这种情况你应该使用thenCombine 方法它接收名为BiFunction的第二个参数,整个参数定义了,当两个CompletableFuture 对象完成计算后,结果如何合并。这里,如果使用thenCombineAsync 会导致BiFunction 中定义的合并操作被提交到线程池中,由另一个任务以异步的方式执行

回到之前的例子,有一家商店的价格是以欧元(EUR)计价的,但是你希望以美元的方式提供给你的客户。可以用异步的方式向商店查询指定商品的价格,同时从远程的汇率服务那里查询到欧元和美元之间的汇率,得到以美元计价的商品价格。

Future<Double> future = CompletableFuture.supplyAsync(() -> shop.getPrice(product))  //创建第一个任务查询商店取得商品的价格
      .thenCombine(CompletableFuture.supplyAsync(() -> exchangeService.getRate(Money.EUR,Money.USD)),  //创建第二个独立任务,查询美元和欧元之间的转换汇率
              (price,rate)-> price * rate ); // 通过乘法整合得到的商品价格和汇率

合并两个互相独立的异步任务:

响应 CompletableFuture 的 completion 事件

一个模拟生成0.5秒至2.5秒随机延迟的方,使用该方法取代原来的固定延迟:

private static final Random random = new Random();

public static void randomDelay(){
    int delay = 500 + random.nextInt(2000);
    try {
        Thread.sleep(delay);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

你实现的findPrices 方法只有在取得所有商店的返回值时才显示商品的价格。而你希望的效果是,只要有商店返回商品价格就在第一时间显示返回值,不再等待那些还未返回的商店(有时甚至会发生超时)

步骤1:

public Stream<CompletableFuture<String>> findPricesStream(String product){
    ExecutorService executors = Executors.newCachedThreadPool();
    return shops.stream().map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product),executors))
            .map(future -> future.thenApply(Quote::parse))
            .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote),executors)));
}

JAVA 8 的CompletableFuture 通过thenAccept 方法提供了一个功能:在每个CompletableFuture 上注册一个操作,该操作会在CompletableFuture 完成执行后使用它的返回值。

findPricesStream("myPhone27S").map(f -> f.thenAccept(s -> Systen.out::println))

由于thenAccept 方法已经定义了: 如何处理 CompletableFuture 返回的结果,一旦CompletableFuture 计算得到结果,它就返回一个CompletableFuture。对于该对象,你能做的非常有限,只能等待其运行结束,不过这也是你期望的。你还希望给最慢的商店一些机会,让它有机会打印输出返回的价格。为了实现这一目的,你可以把构成Stream 的所有CompletableFuture 对象放到一个数组中,等待所有的任务执行完成

步骤2:

CompletableFuture[] futures = findPricesStream("myPhone27S").map(f -> f.thenAccept(s ->
                System.out.println(s + "(done in )" + (System.nanoTime() - start) / 1000000  + "msces)")))
        .toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();

allOf 工厂方法接收一个由 CompletableFuture 构成的数组,数组中的所有CompletableFuture 对象执行完之后,它返回一个CompletableFuture, 这意味着你需要等待最初Stream 中的所有CompletableFuture 对象执行完毕,对allOf 方法返回的 CompletableFuture 执行 join 操作是个不错的注意

然而在另外一些场景中,你可能希望只要CompletableFuture 对象数组中有任何一个执行完毕就不再等待,你可以使用类似的工厂方法anyOf

最终实现allOf:

long start = System.nanoTime();
CompletableFuture[] futures = findPricesStream("myPhone27S").map(f -> f.thenAccept(s ->
                System.out.println(s + "(done in )" + (System.nanoTime() - start) / 1000000  + "msces)")))
        .toArray(size -> new CompletableFuture[size]);

CompletableFuture.allOf(futures).join();
System.out.println("All shops have now responded in " + (System.nanoTime() - start) / 1000000 + "msecs");

执行结果:

myPhone27S price is 2114.719(done in )1700msces)
myPhone27S price is 9692.28(done in )2482msces)
myPhone27S price is 3607.866(done in )3236msces)
myPhone27S price is 3034.25(done in )3341msces)
All shops have now responded in 3341msecs

最终实现anyOf:

long start = System.nanoTime();
CompletableFuture[] futures = findPricesStream("myPhone27S").map(f -> f.thenAccept(s ->
                System.out.println(s + "(done in )" + (System.nanoTime() - start) / 1000000  + "msces)")))
        .toArray(size -> new CompletableFuture[size]);

CompletableFuture.anyOf(futures).join();
System.out.println("All shops have now responded in " + (System.nanoTime() - start) / 1000000 + "msecs");

执行结果:

myPhone27S price is 3650.88(done in )2082msces)
All shops have now responded in 2086msecs
posted @ 2024-02-03 10:51  chuangzhou  阅读(132)  评论(0编辑  收藏  举报