CompletableFuture 專題

滄海一滴發表於2019-03-02

 

/**
 * @Auther: cheng.tang
 * @Date: 2019/3/2
 * @Description:
 */
package com.tangcheng.learning.concurrent;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.concurrent.*;
import java.util.stream.Stream;

/**
 * @Auther: cheng.tang
 * @Date: 2019/3/2
 * @Description:
 */
@Slf4j
public class CompletableFutureTest {


    /**
     * 16:12:01.109 [main] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - start
     * 16:12:01.168 [main] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - 小於coreSize時,每次新建執行緒,都會列印這個日誌。Runtime.getRuntime().availableProcessors():8
     * 16:12:01.171 [main] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - 小於coreSize時,每次新建執行緒,都會列印這個日誌。Runtime.getRuntime().availableProcessors():8
     * 16:12:01.172 [main] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - 小於coreSize時,每次新建執行緒,都會列印這個日誌。Runtime.getRuntime().availableProcessors():8
     * 16:12:03.172 [Thread-2] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - finish :3
     * 16:12:04.172 [Thread-0] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - finish :1
     * 16:12:05.172 [Thread-1] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - finish :2
     * 16:12:05.172 [main] INFO com.tangcheng.learning.concurrent.CompletableFutureTest - last log
     */
    @Test
    public void testCompletableFuture() {

        log.info("start");
        /**
         * runSync不要求有返回值
         */
        CompletableFuture[] completableFutures = Stream.of(1, 2, 3).map(item -> CompletableFuture.supplyAsync(() -> {
            try {
                int timeout = ThreadLocalRandom.current().nextInt(1, 5);
                if (timeout == 1) {
                    throw new IllegalArgumentException("出錯了,為什麼是1");
                }
                TimeUnit.SECONDS.sleep(timeout);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("finish :{}", item);
            return "finish" + item;
        }, executor).exceptionally(ex -> {
                    log.error("exceptionally 一個任務失敗了 item:{},{}", item, ex.getMessage());
                    return "一個任務失敗了" + ex.getMessage();
                }
        )).toArray(CompletableFuture[]::new);
        //allOf():工廠方法接受由CompletableFuture物件構成的陣列,陣列中所有的CompletableFuture完成後它返回一個CompletableFuture<Void>物件。
        CompletableFuture<Void> voidCompletableFuture = CompletableFuture.allOf(completableFutures);
        //等待所有的子執行緒執行完畢
        voidCompletableFuture.join();
        log.info("last log ");
    }

    private static final Executor executor = Executors.newFixedThreadPool(Math.min(Runtime.getRuntime().availableProcessors(), 100), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            log.info("小於coreSize時,每次新建執行緒,都會列印這個日誌。Runtime.getRuntime().availableProcessors():{}", Runtime.getRuntime().availableProcessors());
            Thread t = new Thread(r);
            // 使用守護執行緒---這種方式不會阻止程式關掉
            t.setDaemon(true);
            return t;
        }
    });

}



Future介面的侷限性

Future提供了isDone()方法檢測非同步計算是否已經結束,get()方法等待非同步操作結束,以及獲取計算的結果。但是這些操作依然很難實現:等到所有Future任務完成,通知執行緒獲取結果併合並。下面就一起來看看JDK8中新引入的CompletableFuture。

使用CompletableFuture構建非同步應用

CompletableFuture實現了Future介面,它的complete()方法就相當於結束CompletableFuture物件的執行,並設定變數的值。

/**
 * @author yangfan
 * @date 2017/04/11
 */
public class Shop {

    private static final Random random = new Random();

    private String name;

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


    public String getPrice(String product) {
        double price =  calculatePrice(product);
        Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
        return String.format("%s:%.2f:%s", name, price, code);
    }

    public Future<Double> getPriceAsync1(String product) {
        // 建立CompletableFuture物件,它會包含計算的結果
        CompletableFuture<Double> futurePrice = new CompletableFuture<>();

        // 在另一個執行緒中以非同步方式執行計算
        new Thread(() -> {
            try {
                double price = calculatePrice(product);
                //  需長時間計算的任務結束並得出結果時,設定Future的返回值
                futurePrice.complete(price);
            } catch (Exception e) {
                //異常處理
                futurePrice.completeExceptionally(e);
            }
        }).start();

        // 無需等待還沒結束的計算,直接返回Future物件
        return futurePrice;

    }

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


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

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

    public static void randomDelay() {
        int delay = 500 + random.nextInt(2000);
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) {
        Shop shop = new Shop("BestShop");
        long start = System.nanoTime();
        // 查詢商店,試圖去的商品的價格
        Future<Double> futurePrice = shop.getPriceAsync("my favorte product");
        long invocationTime = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Invocation returned after " + invocationTime + " msecs");

        // 執行更多工,比如查詢其他商店
        //doSomethingElse();
        // 在計算商品價格的同時


        try {
            // 從Future物件中讀取價格,如果價格未知,會發生阻塞
            double price = futurePrice.get();
            System.out.printf("Price is %.2f%n", price);
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }

        long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
        System.out.println("Price returned after " + retrievalTime + " msecs");



    }

    public String getName() {
        return name;
    }
}

 

上面的例子來自於《Java8實戰》,模擬了一個耗時的操作,然後通過CompletableFuture包裝成非同步方法進行呼叫。注意程式碼裡演示了兩種方式,一種自己new一個執行緒再呼叫complete方法,還有就是用CompletableFuture自身提供的工廠方法,CompletableFuture.supplyAsync,它能更容易地完成整個流程,還不用擔心實現的細節。

現在看來好像和Future方式也沒有什麼區別,都是包裝一下最後通過get()方法獲取結果,但是CompletableFuture配合Java8用起來就非常厲害了,它提供了很多方便的方法,下面進行一個演示。

同樣是價格查詢,我們現在接到一個需求,就是獲取一個商品在不同商店的報價,一般來說用傳統的方式就是寫一個for迴圈,遍歷商店然後獲取價格,要想效率快一點我們也可以用並行流的方式來查詢,但是並行流返回的結果是無序的。下面再將非同步也引入,我們可以實現有序的並行操作:

private static List<String> findPrices_1(String product) {
    List<CompletableFuture<String>> priceFutures = shops.stream()
            .map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is " + shop.getPrice(product), executor)).collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}

這裡建立CompletableFuture和呼叫join()方法是兩個不同的流是有原因的,如果只在一個流裡,那麼就沒有非同步的效果了,下一個Future必須等到上一個完成後才會被建立和執行。上面的程式碼執行效率並不會比並行流的效率差。

預設情況下,並行流和CompletableFuture預設都是用固定數目的執行緒,都是取決於Runtime. getRuntime().availableProcessors()的返回值。並行流的執行緒池並不好控制,其本質是內部隱含使用了ForkJoinPool執行緒池,最大併發數可以通過系統變數設定。所以CompletableFuture也就具有了優勢,它允許配置自定義的執行緒池,這也可以為實際應用程式帶來效能上的提升(並行流無法提供的API),CompletableFuture.supplyAsync(Supplier supplier,Executor executor)提供了過載的方法來指定執行器使用自定義的執行緒池。

// 建立一個執行緒池,執行緒池中執行緒的數目為100和商店數目二者中較小的一個值
private static final 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;
    }
});

 

並行–使用流還是CompletableFutures?

現在我們知道對集合進行平行計算有兩種方式:

  1. 轉化為並行流,利用map開展工作。
  2. 取出每一個元素,建立執行緒,在CompletableFuture內對其進行操作

後者提供了更多的靈活性,你可以調整執行緒池的大小,而這能幫助我們確保整體的計算不會因為執行緒都在等待I/O而發生阻塞。

那麼如何選擇呢,建議如下:

  • 進行計算密集型的操作,並且沒有I/O,那麼推薦使用Stream介面,因為實現簡單,同時效率也可能是最高的(如果所有的執行緒都是計算密集型的,那就沒有必要建立比處理器核數更多的執行緒)。
  • 如果並行操作設計等到I/O的操作(網路連線,請求等),那麼使用CompletableFuture靈活性更好,通過控制執行緒數量來優化程式的執行。

CompletableFuture還提供了了一些非常有用的操作例如,thenApply(),thenCompose(),thenCombine()等

  • thenApply()是操作完成後將結果傳入進行轉換
  • thenCompose()是對兩個非同步操作進行串聯,第一個操作完成時,對第一個CompletableFuture物件呼叫thenCompose,並向其傳遞一個函式。當第一個CompletableFuture執行完畢後,它的結果將作為該函式的引數,這個函式的返回值是以第一個CompletableFuture的返回做輸入計算出第二個CompletableFuture物件。
  • thenCombine()會非同步執行兩個CompletableFuture任務,然後等待它們計算出結果後再進行計算。
private static List<String> findPrices(String product) {

    List<CompletableFuture<String>> priceFutures = shops.stream()
            // 以非同步方式取得每個shop中指定產品的原始價格
            .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor))
            // Quote物件存在時,對其返回值進行轉換
            .map(future -> future.thenApply(Quote::parse))
            // 使用另一個非同步任務構造期望的Future,申請折扣
            .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)))
            .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}

 

通常而言,名稱中不帶Async的方法和它的前一個任務一樣,在同一個執行緒中執行;而名稱以Async結尾的方法會將後續的任務提交到一個執行緒池,所以每個任務都是由不同執行緒處理的,例如thenApplyAsync(),thenComposeAsync()等。

最後看一段利用thenAccept()來使用非同步計算結果的程式碼:

// 這裡演示獲取最先返回的資料
public static 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 static void main(String[] args) {

    long start = System.nanoTime();
//        System.out.println(findPrices("myPhone27S"));
    CompletableFuture[] futures = findPricesStream("myPhone27S")
            .map(f -> f.thenAccept(s -> System.out.println(s + " (done in " +
                    ((System.nanoTime() - start) / 1_000_000) + " msecs)")))
            .toArray(CompletableFuture[]::new);
//        CompletableFuture.allOf(futures).join();
    CompletableFuture.anyOf(futures).join();
    long duration = (System.nanoTime() - start) / 1_000_000;
    System.out.println("Done in " + duration + " msecs");

}

這樣就幾乎無需等待findPricesStream的呼叫,實現了一個真正的非同步方法。

https://blog.csdn.net/qq_32331073/article/details/81503475

 

2.執行一個簡單的非同步stage

下面的例子解釋瞭如何建立一個非同步執行Runnable的stage。

static void runAsyncExample() {
    CompletableFuture cf = CompletableFuture.runAsync(() -> {
        assertTrue(Thread.currentThread().isDaemon());
        randomSleep();
    });
    assertFalse(cf.isDone());
    sleepEnough();
    assertTrue(cf.isDone());
}

這個例子想要說明兩個事情:

  1. CompletableFuture中以Async為結尾的方法將會非同步執行
  2. 預設情況下(即指沒有傳入Executor的情況下),非同步執行會使用ForkJoinPool實現,該執行緒池使用一個後臺執行緒來執行Runnable任務。注意這只是特定於CompletableFuture實現,其它的CompletableStage實現可以重寫該預設行為。

https://segmentfault.com/a/1190000013452165?utm_source=index-hottest

 

聯合多個CompletableFuture

另外一組方法允許將多個CF聯合在一起。我們已經看見過靜態方法allOf,當其所有的元件均完成時,它就會處於完成狀態,與之對應的方法也就是anyOf,返回值同樣是void,當其任意一個元件完成時,它就會完成。
除了這兩個方法以外,這個組中其他的方法都是例項方法,它們能夠將receiver按照某種方式與另外一個CF聯合在一起,然後將結果傳遞到給定的函式中。

https://www.cnblogs.com/kexianting/p/8692437.html

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 最佳價格查詢器
 */
public class BestFinder {

    List<Shop> shops = Arrays.asList(
            new Shop("A"),
            new Shop("B"),
            new Shop("C"),
            new Shop("D"),
            new Shop("E"),
            new Shop("F"),
            new Shop("G"),
            new Shop("H"),
            new Shop("I"),
            new Shop("J")
    );

    public void findPricesContinue(String product){
        long st = System.currentTimeMillis();
        Stream<CompletableFuture<String>> futurePrices = shops.stream()
                //首先非同步獲取價格
                .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPriceFormat(product),myExecutor))
                //將獲取的字串解析成物件
                .map(future -> future.thenApply(Quote::parse))
                //使用另一個非同步任務有獲取折扣價格
                .map(future -> future.thenCompose(quote -> CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote),myExecutor)));
        //thenAccept()會在CompletableFuture完成之後使用他的返回值,這裡會持續執行子執行緒
        CompletableFuture[] futures = futurePrices.map(f -> f.thenAccept(s -> {
                                                                            String sout = String.format("%s done in %s mesc",s,(System.currentTimeMillis() - st));
                                                                            System.out.println(sout);
                                                                         }))
                                                  .toArray(size -> new CompletableFuture[size]);
        //allOf()工廠方法接受由CompletableFuture物件構成的陣列,這裡使用其等待所有的子執行緒執行完畢
        CompletableFuture.allOf(futures).join();
    }

  

    /**
     * 非同步查詢
     * 相比並行流的話CompletableFuture更有優勢:可以對執行器配置,設定執行緒池大小
     */
    @SuppressWarnings("all")
    private final Executor myExecutor = 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;
        }
    });
   

https://segmentfault.com/a/1190000012859835

https://segmentfault.com/a/1190000013452165?utm_source=index-hottest

 

相關文章