CompletableFuture

LZC發表於2020-08-06

CompletableFuture是一個可以透過程式設計方式顯式地設定計算結果和狀態以便讓任務結束的Future,並且其可以作為一個CompletionStage(計算階段),當它的計算完成時可以觸發一個函式或者行為;當多個執行緒企圖呼叫同一個CompletableFuture的complete、cancel方式時只有一個執行緒會成功。CompletableFuture除了含有可以直接操作任務狀態和結果的方法外,還實現了CompletionStage介面的一些方法,這些方法遵循:

● 當CompletableFuture任務完成後,同步使用任務執行執行緒來執行依賴任務結果的函式或者行為。

● 所有非同步的方法在沒有顯式指定Executor引數的情形下都是複用ForkJoinPool. commonPool()執行緒池來執行。

● 所有CompletionStage方法的實現都是相互獨立的,以便一個方法的行為不會因為過載了其他方法而受影響。

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 自定義一個執行緒池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,
                1,
                0L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(10),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        CompletableFuture<String> future = new CompletableFuture<>();

        threadPoolExecutor.submit(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            future.complete("success......");
        });
        System.out.println(future.get());
        System.out.println("main end......");

        threadPoolExecutor.shutdown();
    }
}

如上所述,這裡使用CompletableFuture實現了通知等待模型,主執行緒呼叫future的get()方法等待future返回結果,一開始由於future結果沒有設定,所以主執行緒被阻塞掛起,等非同步任務休眠3s,然後呼叫future的complete方法模擬主執行緒等待的條件完成,這時候主執行緒就會從get()方法返回。

runAsync

基於runAsync系列方法實現無返回值的非同步計算:當你想非同步執行一個任務,並且不需要任務的執行結果時可以使用該方法,比如非同步打日誌,非同步做訊息通知等:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture future = CompletableFuture.runAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("over......");
        });
        // 同步等待非同步執行結果
        System.out.println(future.get());
    }
}

以上程式碼呼叫返回的future的get()方法企圖等待future任務執行完畢,由於runAsync方法不會有返回值,所以當任務執行完畢後,設定future的結果為null,即等任務執行完畢後返回null。

需要注意的是,在預設情況下,runAsync(Runnable runnable)方法是使用整個JVM內唯一的ForkJoinPool.commonPool()執行緒池來執行非同步任務的,使用runAsync (Runnable runnable, Executor executor)方法允許我們使用自己制定的執行緒池來執行非同步任務。我們建立了一個自己的執行緒池bizPoolExecutor,在呼叫runAsync方法提交非同步任務時,把其作為第二引數進行傳遞,則非同步任務執行時會使用bizPoolExecutor中的執行緒執行,具體程式碼如下所示。

public class Test {
    // 自定義一個執行緒池
    public static ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(
            1,
            1,
            0L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(10),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture future = CompletableFuture.runAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("over......");
        },bizPoolExecutor);
        // 同步等待非同步執行結果
        System.out.println(future.get());
        // 關閉執行緒池
        bizPoolExecutor.shutdown();
    }
}

supplyAsync

基於supplyAsync系列方法實現有返回值的非同步計算:當你想非同步執行一個任務,並且需要任務的執行結果時可以使用該方法,比如非同步對原始資料進行加工,並需要獲取到被加工後的結果等。

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("over......");
            return "success......";
        });
        // 同步等待非同步執行結果
        System.out.println(future.get());
    }
}

需要注意的是,在預設情況下,supplyAsync(Suppliersupplier)方法是使用整個JVM內唯一的ForkJoinPool.commonPool()執行緒池來執行非同步任務的,使用supply-Async(Supplier supplier, Executor executor)方法允許我們使用自己制定的執行緒池來執行非同步任務,程式碼如下:

public class Test {
    // 自定義一個執行緒池
    public static ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(
            1,
            1,
            0L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(10),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("over......");
            return "success......";
        }, bizPoolExecutor);
        // 同步等待非同步執行結果
        System.out.println(future.get());
        // 關閉執行緒池
        bizPoolExecutor.shutdown();
    }
}

thenRun

基於thenRun實現非同步任務A,執行完畢後,啟用非同步任務B執行,需要注意的是,這種方式啟用的非同步任務B是拿不到任務A的執行結果的:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            return "A";
        });

        CompletableFuture futureB = futureA.thenRun(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B over......");
        });
        // 同步等待非同步執行結果
        System.out.println(futureB.get());
    }
}

在futureB上呼叫get()方法也會返回null,因為回撥事件是沒有返回值的。

預設情況下futureA對應的非同步任務和在oneFuture上新增的回撥事件都是使用ForkJoinPool.commonPool()中的同一個執行緒來執行的,大家可以使用thenRunAsync (Runnable action,Executor executor)來指定設定的回撥事件使用自定義執行緒池執行緒來執行,也就是futureA對應的任務與在其上設定的回撥執行將不會在同一個執行緒中執行。

thenAccept

基於thenAccept實現非同步任務A,執行完畢後,啟用非同步任務B執行,需要注意的是,這種方式啟用的非同步任務B是可以拿到任務A的執行結果的:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            return "A";
        });

        CompletableFuture futureB = futureA.thenAccept(new Consumer<String>() {
            // 這裡的 result 為 futureA 返回的結果
            @Override
            public void accept(String result) {
                System.out.println("previous result: " + result);
                try {
                    // 休眠3秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("B over......");
            }
        });
        // 同步等待非同步執行結果
        System.out.println(futureB.get());
    }
}

需要注意的是,這裡可以在回撥的方法accept(String result)的引數t中來獲取futureA對應的任務結果,另外需要注意的是,由於accept(String result)方法沒有返回值,所以在futureB上呼叫get()方法最終也會返回null。

在預設情況下,futureA對應的非同步任務和在futureA上新增的回撥事件都是使用ForkJoinPool.commonPool()中的同一個執行緒來執行的,大家可以使用thenAccept-Async(Consumer<? super T> action, Executor executor)來指定設定的回撥事件使用自定義執行緒池執行緒來執行,也就是futureA對應的任務與在其上設定的回撥執行將不會在同一個執行緒中執行。

thenApply

基於thenApply實現非同步任務A,執行完畢後,啟用非同步任務B執行。需要注意的是,這種方式啟用的非同步任務B是可以拿到任務A的執行結果的,並且可以獲取到非同步任務B的執行結果。

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立非同步任務
        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            return "A";
        });

        CompletableFuture<String> futureB = futureA.thenApply(new Function<String, String>() {
            // 這裡的 result 為 futureA 返回的結果
            @Override
            public String apply(String result) {
                System.out.println("previous result: " + result);
                try {
                    // 休眠3秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("B over......");
                return result + " => B";
            }
        });
        // 同步等待非同步執行結果
        System.out.println(futureB.get());
    }
}

需要注意的是,這裡可以在回撥方法apply(String result)的引數t中獲取futureA對應的任務結果,另外需要注意的是,由於apply(String result)方法有返回值,所以在futureB上呼叫get()方法最終也會返回回撥方法返回的值。

預設情況下futureA對應的非同步任務和在futureA上新增的回撥事件都是使用ForkJoinPool.commonPool()中的同一個執行緒來執行的,大家可以使用thenApplyAsync (Function<?super T, ? extends U> fn, Executor executor)來指定設定的回撥事件使用自定義執行緒池執行緒來執行,也就是futureA對應的任務與在其上設定的回撥執行將不會在同一個執行緒中執行。

whenComplete

基於whenComplete設定回撥函式,當非同步任務執行完畢後進行回撥,不會阻塞呼叫執行緒:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.建立非同步任務
        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            // 1.2 放回計算結果
            return "A";
        });
        // 2. 新增回撥函式
        futureA.whenComplete(new BiConsumer<String, Throwable>() {
            @Override
            public void accept(String result, Throwable throwable) {
                System.out.println("previous result: " + result);
                try {
                    // 休眠3秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("B over......");
            }
        });
        System.out.println("main ......");
        // 3. 掛起當前執行緒,等待非同步任務執行完畢
        Thread.currentThread().join();
    }
}

這裡程式碼1開啟了一個非同步任務,任務內先休眠3s,然後程式碼1.2返回計算結果;程式碼2則在返回的futureA上呼叫whenComplete設定一個回撥函式,然後main執行緒就返回了。在整個非同步任務的執行過程中,main函式所線上程是不會被阻塞的,等非同步任務執行完畢後會回撥設定的回撥函式。這裡程式碼3掛起了main函式所線上程,是因為具體執行非同步任務的是ForkJoin的commonPool執行緒池,其中執行緒都是Deamon執行緒,所以,當唯一的使用者執行緒main執行緒退出後整個JVM程式就退出了,會導致非同步任務得不到執行。

如上所述,當我們使用CompletableFuture實現非同步程式設計時,大多數時候是不需要顯式建立執行緒池,並投遞任務到執行緒池內的。我們只需要簡單地呼叫CompletableFuture的runAsync或者supplyAsync等方法把非同步任務作為引數即可,其內部會使用ForkJoinPool執行緒池來進行非同步執行的支援,這大大簡化了我們非同步程式設計的負擔,實現了宣告式程式設計(告訴程式我要執行非同步任務,但是具體怎麼實現我不需要管),當然如果你想使用自己的執行緒池來執行任務,也是可以非常方便地進行設定的。

CompletableFuture功能強大的原因之一是其可以讓兩個或者多個Completable-Future進行運算來產生結果,下面我們來看其提供的幾組函式:

thenCompose

基於thenCompose實現當一個CompletableFuture執行完畢後,執行另外一個CompletableFuture:

public class Test {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> result = futureA("123").thenCompose(str -> futureB(str));
        System.out.println(result.get());
    }

    public static CompletableFuture<String> futureA(String str) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            // 返回計算結果
            return "A " + str;
        });
    }

    public static CompletableFuture<String> futureB(String str) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B over......");
            // 返回計算結果
            return "B " + str;
        });
    }
}

thenCombine

基於thenCombine實現當兩個併發執行的CompletableFuture任務都完成後,使用兩者的結果作為引數再執行一個非同步任務

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFuture<String> result = futureA("123").thenCombine(futureB("456"), (one, two) -> {
            return one + " " + two;
        });
        System.out.println(result.get());
    }

    public static CompletableFuture<String> futureA(String str) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            // 返回計算結果
            return "A " + str;
        });
    }

    public static CompletableFuture<String> futureB(String str) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠2秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B over......");
            // 返回計算結果
            return "B " + str;
        });
    }
}

allOf

基於allOf等待多個併發執行的CompletableFuture任務執行完畢:

public class Test {
    // 自定義一個執行緒池
    public static ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(
            1,
            1,
            0L,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(10),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.建立非同步任務
        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            // 1.2 返回計算結果
            return "A";
        });
        // 2.建立非同步任務
        CompletableFuture<String> futureB = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
                System.out.println("B over......");
            // 2.2 返回計算結果
            return "B";
        });

        List<CompletableFuture<String>> futureList = new ArrayList<>();
        futureList.add(futureA);
        futureList.add(futureB);

        CompletableFuture result = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[futureList.size()]));

        // 3. 等待所有 future 完成
        System.out.println(result.get());
    }
}

用allOf方法把多個CompletableFuture轉換為一個result,程式碼3在result上呼叫get()方法會阻塞呼叫執行緒,直到futureList列表中所有任務執行完畢才返回。

anyOf

基於anyOf等多個併發執行的CompletableFuture任務中有一個執行完畢就返回:

public class Test {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1.建立非同步任務
        CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("A over......");
            // 1.2 返回計算結果
            return "A";
        });
        // 2.建立非同步任務
        CompletableFuture<String> futureB = CompletableFuture.supplyAsync(() -> {
            try {
                // 休眠3秒
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
                System.out.println("B over......");
            // 2.2 返回計算結果
            return "B";
        });

        List<CompletableFuture<String>> futureList = new ArrayList<>();
        futureList.add(futureA);
        futureList.add(futureB);

        CompletableFuture result = CompletableFuture.anyOf(futureList.toArray(new CompletableFuture[futureList.size()]));

        // 3. 等待某一個 future 完成
        System.out.println(result.get());
    }
}

呼叫anyOf方法把多個CompletableFuture轉換為一個result,程式碼3在result上呼叫get()方法會阻塞呼叫執行緒,直到futureList列表中有一個任務執行完畢才返回。

正常同步呼叫程式碼如下所示

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 生成ip列表
        List<String> ipList = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            ipList.add("192.168.0." + i);
        }
        // 2. 發起廣播呼叫
        Long start = System.currentTimeMillis();
        List<String> result = new ArrayList<>();
        for (String ip : ipList) {
            result.add(rpcCall(ip, ip));
        }
        result.stream().forEach(s -> System.out.println(s));
        System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    public static String rpcCall(String ip, String param) {
        System.out.println(ip + ":" + param);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return param;
    }
}

下面我們將Stream和CompletableFuture結合使用

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 生成ip列表
        List<String> ipList = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            ipList.add("192.168.0." + i);
        }
        // 2. 併發呼叫
        Long start = System.currentTimeMillis();
        List<CompletableFuture<String>> futureList = ipList.stream()
                .map(ip -> CompletableFuture.supplyAsync(() -> rpcCall(ip, ip))) // 同步轉非同步
                .collect(Collectors.toList());// 收集結果
        // 3. 等待所有非同步任務執行完畢
        List<String> result = futureList.stream().map(future -> future.join()).collect(Collectors.toList());
        result.stream().forEach(s -> System.out.println(s));
        System.out.println("cost: " + (System.currentTimeMillis() - start));
    }
    public static String rpcCall(String ip, String param) {
        System.out.println(ip + ":" + param);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return param;
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章