非同步技巧之CompletableFuture

咖啡拿鐵發表於2018-07-11

如果喜歡微信閱讀,想了解更多java知識,系統設計,分散式中介軟體等可以關注我的微訊號: java喝咖啡,當然還有更多福利等著你。

1.Future介面

1.1 什麼是Future?

在jdk的官方的註解中寫道

A {@code Future} represents the result of an asynchronous
 * computation.  Methods are provided to check if the computation is
 * complete, to wait for its completion, and to retrieve the result of
 * the computation.
複製程式碼

在上面的註釋中我們能知道Future用來代表非同步的結果,並且提供了檢查計算完成,等待完成,檢索結果完成等方法。簡而言之就是提供一個非同步運算結果的一個建模。它可以讓我們把耗時的操作從我們本身的呼叫執行緒中釋放出來,只需要完成後再進行回撥。就好像我們去飯店裡面吃飯,不需要你去煮飯,而你這個時候可以做任何事,然後飯煮好後就會回撥你去吃。

1.2 JDK8以前的Future

在JDK8以前的Future使用比較簡單,我們只需要把我們需要用來非同步計算的過程封裝在Callable或者Runnable中,比如一些很耗時的操作(不能佔用我們的呼叫執行緒時間的),然後再將它提交給我們的執行緒池ExecutorService。程式碼例子如下:

public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<String> future = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return Thread.currentThread().getName();
            }
        });

        doSomethingElse();//在我們非同步操作的同時一樣可以做其他操作
        try {
            String res = future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
複製程式碼

上面展示了我們的執行緒可以併發方式呼叫另一個執行緒去做我們耗時的操作。當我們必須依賴我們的非同步結果的時候我們就可以呼叫get方法去獲得。當我們呼叫get方法的時候如果我們的任務完成就可以立馬返回,但是如果任務沒有完成就會阻塞,直到超時為止。

Future底層是怎麼實現的呢? 我們首先來到我們ExecutorService的程式碼中submit方法這裡會返回一個Future

public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
複製程式碼

在sumbmit中會對我們的Callable進行包裝封裝成我們的FutureTask,我們最後的Future其實也是Future的實現類FutureTask,FutureTask實現了Runnable介面所以這裡直接呼叫execute。在FutureTask程式碼中的run方法程式碼如下:

public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } 
        .......
    }
複製程式碼

可以看見當我們執行完成之後會set(result)來通知我們的結果完成了。set(result)程式碼如下:

protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
複製程式碼

首先用CAS置換狀態為完成,以及替換結果,當替換結果完成之後,才會替換為我們的最終狀態,這裡主要是怕我們設定完COMPLETING狀態之後最終值還沒有真正的賦值出去,而我們的get就去使用了,所以還會有個最終狀態。我們的get()方法的程式碼如下:

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
複製程式碼

首先獲得當前狀態,然後判斷狀態是否完成,如果沒有完成則進入awaitDone迴圈等待,這也是我們阻塞的程式碼,然後返回我們的最終結果。

1.2.1缺陷

我們的Future使用很簡單,這也導致瞭如果我們想完成一些複雜的任務可能就比較難。比如下面一些例子:

  • 將兩個非同步計算合成一個非同步計算,這兩個非同步計算互相獨立,同時第二個又依賴第一個的結果。
  • 當Future集合中某個任務最快結束時,返回結果。
  • 等待Future結合中的所有任務都完成。
  • 通過程式設計方式完成一個Future任務的執行。
  • 應對Future的完成時間。也就是我們的回撥通知。

1.3CompletableFuture

CompletableFuture是JDK8提出的一個支援非阻塞的多功能的Future,同樣也是實現了Future介面。

1.3.1CompletableFuture基本實現

下面會寫一個比較簡單的例子:

public static void main(String[] args) {
        CompletableFuture<String> completableFuture = new CompletableFuture<>();
        new Thread(()->{
            completableFuture.complete(Thread.currentThread().getName());
        }).start();
        doSomethingelse();//做你想做的其他操作
        
        try {
            System.out.println(completableFuture.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
複製程式碼

用法上來說和Future有一點不同,我們這裡fork了一個新的執行緒來完成我們的非同步操作,在非同步操作中我們會設定值,然後在外部做我們其他操作。在complete中會用CAS替換result,然後當我們get如果可以獲取到值得時候就可以返回了。

1.3.2錯誤處理

上面介紹了正常情況下但是當我們在我們非同步執行緒中產生了錯誤的話就會非常的不幸,錯誤的異常不會告知給你,會被扼殺在我們的非同步執行緒中,而我們的get方法會被阻塞。

對於我們的CompletableFuture提供了completeException方法可以讓我們返回我們非同步執行緒中的異常,程式碼如下:

public static void main(String[] args) {
        CompletableFuture<String> completableFuture = new CompletableFuture<>();
        new Thread(()->{
            completableFuture.completeExceptionally(new RuntimeException("error"));
            completableFuture.complete(Thread.currentThread().getName());
        }).start();
//        doSomethingelse();//做你想做的耗時操作

        try {
            System.out.println(completableFuture.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
--------------
輸出:
java.util.concurrent.ExecutionException: java.lang.RuntimeException: error
	at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
	at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1887)
	at futurepackge.jdk8Future.main(jdk8Future.java:19)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.lang.RuntimeException: error
	at futurepackge.jdk8Future.lambda$main$0(jdk8Future.java:13)
	at futurepackge.jdk8Future$$Lambda$1/1768305536.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
複製程式碼

在我們新建的非同步執行緒中直接New一個異常丟擲,在我們客戶端中依然可以獲得異常。

1.3.2工廠方法建立CompletableFuture

我們的上面的程式碼雖然不復雜,但是我們的java8依然對其提供了大量的工廠方法,用這些方法更容易完成整個流程。如下面的例子:

public static void main(String[] args) {
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() ->{
                return Thread.currentThread().getName();
        });
//        doSomethingelse();//做你想做的耗時操作

        try {
            System.out.println(completableFuture.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
---------
輸出:
ForkJoinPool.commonPool-worker-1
複製程式碼

上面的例子通過工廠方法supplyAsync提供了一個Completable,在非同步執行緒中的輸出是ForkJoinPool可以看出當我們不指定執行緒池的時候會使用ForkJoinPool,而我們上面的compelte的操作在我們的run方法中做了,原始碼如下:

public void run() {
            CompletableFuture<T> d; Supplier<T> f;
            if ((d = dep) != null && (f = fn) != null) {
                dep = null; fn = null;
                if (d.result == null) {
                    try {
                        d.completeValue(f.get());
                    } catch (Throwable ex) {
                        d.completeThrowable(ex);
                    }
                }
                d.postComplete();
            }
        }
複製程式碼

上面程式碼中通過d.completeValue(f.get());設定了我們的值。同樣的構造方法還有runasync等等。

1.3.3計算結果完成時的處理

當CompletableFuture計算結果完成時,我們需要對結果進行處理,或者當CompletableFuture產生異常的時候需要對異常進行處理。有如下幾種方法:

public CompletableFuture<T> 	whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> 	whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> 	whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T>     exceptionally(Function<Throwable,? extends T> fn)
複製程式碼

上面的四種方法都返回了CompletableFuture,當我們Action執行完畢的時候,future返回的值和我們原始的CompletableFuture的值是一樣的。上面以Async結尾的會在新的執行緒池中執行,上面沒有一Async結尾的會在之前的CompletableFuture執行的執行緒中執行。例子程式碼如下:

public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(jdk8Future::getMoreData);
        Future<Integer> f = future.whenComplete((v, e) -> {
            System.out.println(Thread.currentThread().getName());
            System.out.println(v);
        });
        System.out.println("Main" + Thread.currentThread().getName());
        System.out.println(f.get());
    }
複製程式碼

exceptionally方法返回一個新的CompletableFuture,當原始的CompletableFuture丟擲異常的時候,就會觸發這個CompletableFuture的計算,呼叫function計算值,否則如果原始的CompletableFuture正常計算完後,這個新的CompletableFuture也計算完成,它的值和原始的CompletableFuture的計算的值相同。也就是這個exceptionally方法用來處理異常的情況。

1.3.4計算結果完成時的轉換

上面我們討論瞭如何計算結果完成時進行的處理,接下來我們討論如何對計算結果完成時,對結果進行轉換。

public <U> CompletableFuture<U> 	thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> 	thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> 	thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
複製程式碼

這裡同樣也是返回CompletableFuture,但是這個結果會由我們自定義返回去轉換他,同樣的不以Async結尾的方法由原來的執行緒計算,以Async結尾的方法由預設的執行緒池ForkJoinPool.commonPool()或者指定的執行緒池executor執行。Java的CompletableFuture類總是遵循這樣的原則,下面就不一一贅述了。 例子程式碼如下:

public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 10;
        });
        CompletableFuture<String> f = future.thenApply(i ->i+1 ).thenApply(i-> String.valueOf(i));
        System.out.println(f.get());
    }
複製程式碼

上面的最終結果會輸出11,我們成功將其用兩個thenApply轉換為String。

1.3.5計算結果完成時的消費

上面已經講了結果完成時的處理和轉換,他們最後的CompletableFuture都會返回對應的值,這裡還會有一個只會對計算結果消費不會返回任何結果的方法。

public CompletableFuture<Void> 	thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> 	thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> 	thenAcceptAsync(Consumer<? super T> action, Executor executor)
複製程式碼

函式介面為Consumer,就知道了只會對函式進行消費,例子程式碼如下:

public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 10;
        });
        future.thenAccept(System.out::println);
    }
複製程式碼

這個方法用法很簡單我就不多說了.Accept家族還有個方法是用來合併結果當兩個CompletionStage都正常執行的時候就會執行提供的action,它用來組合另外一個非同步的結果。

public <U> CompletableFuture<Void> 	thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action)
public <U> CompletableFuture<Void> 	thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action)
public <U> CompletableFuture<Void> 	thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T,? super U> action, Executor executor)
public     CompletableFuture<Void> 	runAfterBoth(CompletionStage<?> other,  Runnable action)
複製程式碼

runAfterBoth是當兩個CompletionStage都正常完成計算的時候,執行一個Runnable,這個Runnable並不使用計算的結果。 示例程式碼如下:

public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 10;
        });
        System.out.println(future.thenAcceptBoth(CompletableFuture.supplyAsync(() -> {
            return 20;
        }),(x,y) -> System.out.println(x+y)).get());
    }
複製程式碼

CompletableFuture也提供了執行Runnable的辦法,這裡我們就不能使用我們future中的值了。

public CompletableFuture<Void> 	thenRun(Runnable action)
public CompletableFuture<Void> 	thenRunAsync(Runnable action)
public CompletableFuture<Void> 	thenRunAsync(Runnable action, Executor executor)
複製程式碼

1.3.6對計算結果的組合

首先是介紹一下連線兩個future的方法:

public <U> CompletableFuture<U> 	thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> 	thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> 	thenComposeAsync(Function<? super T,? extends CompletionStage<U>> fn, Executor executor)
複製程式碼

對於Compose可以連線兩個CompletableFuture,其內部處理邏輯是當第一個CompletableFuture處理沒有完成時會合併成一個CompletableFuture,如果處理完成,第二個future會緊接上一個CompletableFuture進行處理。

public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 10;
        });
        System.out.println(future.thenCompose(i -> CompletableFuture.supplyAsync(() -> { return i+1;})).get());
    }
複製程式碼

我們上面的thenAcceptBoth講了合併兩個future,但是沒有返回值這裡將介紹一個有返回值的方法,如下:

public <U,V> CompletableFuture<V> 	thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> 	thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> 	thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor)
複製程式碼

例子比較簡單如下:

public static void main(String[] args) throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            return 10;
        });
        CompletableFuture<String> f = future.thenCombine(CompletableFuture.supplyAsync(() -> {
            return 20;
        }),(x,y) -> {return "計算結果:"+x+y;});
        System.out.println(f.get());
    }
複製程式碼

上面介紹了兩個future完成的時候應該完成的工作,接下來介紹任意一個future完成時需要執行的工作,方法如下:

public CompletableFuture<Void> 	acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> 	acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> 	acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)
public <U> CompletableFuture<U> 	applyToEither(CompletionStage<? extends T> other, Function<? super T,U> fn)
public <U> CompletableFuture<U> 	applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn)
public <U> CompletableFuture<U> 	applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn, Executor executor)
複製程式碼

上面兩個是一個是純消費不返回結果,一個是計算後返回結果。

1.3.6其他方法

public static CompletableFuture<Void> 	    allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> 	anyOf(CompletableFuture<?>... cfs)
複製程式碼

allOf方法是當所有的CompletableFuture都執行完後執行計算。

anyOf方法是當任意一個CompletableFuture執行完後就會執行計算,計算的結果相同。

1.3.7建議

CompletableFuture和Java8的Stream搭配使用對於一些並行訪問的耗時操作有很大的效能提高,可以自行了解。

最後這篇文章被我收錄於JGrowing,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:github.com/javagrowing… 麻煩給個小星星喲。

想要獲取更多資訊請關注技術公眾號

非同步技巧之CompletableFuture

相關文章