CompletableFuture深入理解

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搭配使用對于一些并行訪問的耗時操作有很大的性能提高,可以自行了解。

2.參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 一、并發 進程:每個進程都擁有自己的一套變量 線程:線程之間共享數據 1.線程 Java中為多線程任務提供了很多的...
    SeanMa閱讀 2,508評論 0 11