Java8笔记(6)

Java8笔记(6)

CompletableFuture:组合式异步编程

如果你的意图是实现并发,而非并行,或者你的主要目标是在同一个CPU上执行几个松耦合的任务,充分利用CPU的核,让其足够忙碌,从而最大化程序的吞吐量,那么你其实真正想做的是避免因为等待远程服务的返回,或者对数据库的查询,而阻塞线程的执行,浪费宝贵的计算资源,因为这种等待的时间很可能相当长

Future 接口,尤其是它的新版实现 CompletableFuture ,是处理这种情况的利器

Future 接口

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

你可以把它想象成这样的场景:你拿了一袋子衣服到你中意的干洗店去洗。干洗店的员工会给你张发票,告诉你什么时候你的衣服会洗好(这就是一个 Future 事件)。衣服干洗的同时,你可以去做其他的事情

使用 Future ,通常你只需要将耗时的操作封装在一个 Callable 对象中,再将它提交给 ExecutorService ,就万事大吉了

使用 Future 以异步的方式执行一个耗时的操作


public class M1 {

    //执//行耗时的操作
    public static Double doSomeLongComputation(){
        return 1.0;
    }

    //做其他的事情
    public static void doSomethingElse(){}


    public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {

        ExecutorService executorService = Executors.newCachedThreadPool();

        Future<Double> future = executorService.submit(new Callable<Double>() {

//            以异步方式在//新的线程中执//行耗时的操作
            @Override
            public Double call() throws Exception {
                return doSomeLongComputation();
            }
        });

//        异步操作进行的同时,//你可以做其他的事情
        doSomethingElse();

//        获取异步操作的//结果,如果最终被//阻塞,无法得到结//果,那么在最多等//待1秒钟之后退出
        Double res = future.get(1, TimeUnit.SECONDS);



    }

}


这种编程方式让你的线程可以在 ExecutorService 以并发方式调用另一个线程执行耗时操作的同时,去执行一些其他的任务。接着,如果你已经运行到没有异步操作的结果就无法继续任何有意义的工作时,可以调用它的 get 方法去获取操作的结果。如果操作已经完成,该方法会立刻返回操作的结果,否则它会阻塞你的线程,直到操作完成,返回相应的结果

如果该长时间运行的操作永远不返回了会怎样?为了处理这种可能性,虽然 Future 提供了一个无需任何参数的 get 方法,我们还是推荐大家使用重载版本的 get 方法,它接受一个超时的参数,通过它,你可以定义你的线程等待 Future 结果的最长时间

使用 CompletableFuture 构建异步应用

创建一个名为“最佳价格查询器”(best-price-finder)的应用,它会查询多个在线商店,依据给定的产品或服务找出最低的价格

同步API与异步API

同步API其实只是对传统方法调用的另一种称呼:你调用了某个方法,调用方在被调用方运行的过程中会等待,被调用方运行结束返回,调用方取得被调用方的返回值并继续运行。即
使调用方和被调用方在不同的线程中运行,调用方还是需要等待被调用方结束运行,这是 阻塞式调用这个名词的由来

异步API会直接返回,或者至少在被调用方计算完成之前,将它剩余的计算任务交给另一个线程去做,该线程和调用方是异步的——这就是 非阻塞式调用的由来

实现异步 API

商店应该声明依据指定产品名称返回价格的方法


public class Shop {
    public double getPrice(String product) {
    // 待实现
    }
}

该方法的内部实现会查询商店的数据库,但也有可能执行一些其他耗时的任务,比如联系其
他外部服务(比如,商店的供应商,或者跟制造商相关的推广折扣)

采用 delay 方法模拟这些长期运行的方法的执行,它会人为地引入1秒钟的延迟


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


getPrice 方法会调用 delay 方法,并返回一个随机计算的值


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

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


很明显,这个API的使用者调用该方法时,它依旧会被阻塞。为等待同步事件完成而等待1秒钟,这是无法接受的,尤其是考虑到最佳价格查询器对网络中的所有商店都要重复这种操作

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



public class Shop {

    Random random = new Random(55);

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

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


    public Future<Double> getPriceAsync(String product){

//        创建 CompletableFuture//对象,它会包含计算的结果
        CompletableFuture<Double> future = new CompletableFuture<>();

        new Thread(()->
        {
            double price = calculatePrice(product);

//            需长时间计算的任务结//束并得出结果时,设置//Future 的返回值
            future.complete(price);
        }).start();

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

    }
}

创建了一个代表异步计算的 CompletableFuture 对象实例,它在计算完成时会包含计算的结果。接着,你调用 fork 创建了另一个线程去执行实际的价格计算工作,不等该耗时计算任务结束,直接返回一个 Future 实例。当请求的产品价格最终计算得出时,你可以使用它的 complete 方法,结束 completableFuture 对象的运行,并设置变量的值

使用异步API


public class M1 {


    public static void doSomethingElse(){}

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Shop shop = new Shop();

        long s = System.nanoTime();

        // 查询商店,试图取得商品的价格
        Future<Double> future = shop.getPriceAsync("my");


        long invocationTime = ((System.nanoTime() - s) / 1_000_000);

        System.out.println("Invocation returned after " + invocationTime
                + " msecs");

        // 执行更多任务,比如查询其他商店
        doSomethingElse();
        // 从 Future 对象中读取价格,如果价格未知,会发生阻塞
        double price = future.get();

        System.out.printf("Price is %.2f%n", price);

        long retrievalTime = ((System.nanoTime() - s) / 1_000_000);

        System.out.println("Price returned after " + retrievalTime + " msecs");

    }
}

客户向商店查询了某种商品的价格。由于商店提供了异步API,该次调用立刻返回了一个 Future 对象,通过该对象客户可以在将来的某个时刻取得商品的价格。这种方式下,客户在进行商品价格查询的同时,还能执行一些其他的任务,比如查询其他家商店中商品的价格,不会呆呆地阻塞在那里等待第一家商店返回请求的结果。最后,如果所有有意义的工作都已经完成,客户所有要执行的工作都依赖于商品价格时,再调用 Future 的 get 方法。执行了这个操作后,客户要么获得 Future 中封装的值(如果异步任务已经完成),要么发生阻塞,直到该异步任务完成,期望的值能够访问

错误处理

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

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

抛出 CompletableFuture 内的异常


public class Shop {


    Random random = new Random(55);

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

    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( () -> {
            try {
                double price = calculatePrice(product);

//                如果价格计算正常结//束,完成 Future 操作//并设置商品价格
                futurePrice.complete(price);
            } catch (Exception ex) {
//                否则就抛出导致失//败的异常,完成这//次 Future 操作
                futurePrice.completeExceptionally(ex);
            }
        }).start();
        return futurePrice;
    }

//    客户端现在会收到一个 ExecutionException 异常,该异常接收了一个包含失败原因的
//Exception 参数,即价格计算方法最初抛出的异常

}

使用工厂方法 supplyAsync 创建 CompletableFuture

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


public class Shop {

    Random random = new Random(55);

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

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

//supplyAsync 方法接受一个生产者( Supplier )作为参数,返回一个 CompletableFuture
//对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会交由 ForkJoinPool
//池中的某个执行线程( Executor )运行,但是你也可以使用 supplyAsync 方法的重载版本,传
//递第二个参数指定不同的执行线程执行生产者方法。一般而言,向 CompletableFuture 的工厂
//方法传递可选参数,指定生产者方法的执行线程是可行的
    public Future<Double> getPriceAsync(String product) {
        return CompletableFuture.supplyAsync(() -> calculatePrice(product));
    }
}

免受阻塞

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


public class M1 {

    static List<Shop> shops = Arrays.asList(
            new Shop("BestPrice"),
            new Shop("LetsSaveBig"),
            new Shop("MyFavoriteShop"),
            new Shop("BuyItAll")
    );


//    采用顺序查询所有商店的方式实现的 findPrices 方法
    public static List<String> findPrices(String product) {
        return shops.stream()
                .map(shop -> String.format("%s price is %.2f",
                        shop.getShop_name(), shop.getPriceAsync(product)))
                .collect(toList());
    }






    public static void main(String[] args) {


        long start = System.nanoTime();
        System.out.println(findPrices("myPhone27S"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");



    }
}

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


public class M1 {

    static List<Shop> shops = Arrays.asList(
            new Shop("BestPrice"),
            new Shop("LetsSaveBig"),
            new Shop("MyFavoriteShop"),
            new Shop("BuyItAll")
    );


//对 findPrices 进行并行操作
    public static List<String> findPrices_1(String product) {
        return shops.parallelStream()
                .map(shop -> String.format("%s price is %.2f",
                        shop.getShop_name(), shop.getPriceAsync(product)))
                .collect(toList());
    }




    public static void main(String[] args) {


        long start = System.nanoTime();
        System.out.println(findPrices("myPhone27S"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");



    }
}


使用 CompletableFuture 发起异步请求


public class M1 {

    static List<Shop> shops = Arrays.asList(
            new Shop("BestPrice"),
            new Shop("LetsSaveBig"),
            new Shop("MyFavoriteShop"),
            new Shop("BuyItAll")
    );

//使用 CompletableFuture 实现 findPrices 方法
    public List<String> findPrices_2(String product) {
        List<CompletableFuture<String>> priceFutures =
                shops.stream()
                        .map(shop -> CompletableFuture.supplyAsync(
                                () -> shop.getShop_name() + " price is " +
                                        shop.getPriceAsync(product)))
                        .collect(Collectors.toList());
        return priceFutures.stream()
                .map(CompletableFuture::join)
                .collect(toList());
    }

    public static void main(String[] args) {


        long start = System.nanoTime();
        System.out.println(findPrices("myPhone27S"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");



    }
}

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

发布了229 篇原创文章 · 获赞 62 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/Coder_py/article/details/104099251