非同步神器:CompletableFuture實現原理和使用場景

好好學java發表於2022-02-20

1.概述

CompletableFuture是jdk1.8引入的實現類。擴充套件了Future和CompletionStage,是一個可以在任務完成階段觸發一些操作Future。簡單的來講就是可以實現非同步回撥。

2.為什麼引入CompletableFuture

對於jdk1.5的Future,雖然提供了非同步處理任務的能力,但是獲取結果的方式很不優雅,還是需要通過阻塞(或者輪訓)的方式。如何避免阻塞呢?其實就是註冊回撥。

業界結合觀察者模式實現非同步回撥。也就是當任務執行完成後去通知觀察者。比如Netty的ChannelFuture,可以通過註冊監聽實現非同步結果的處理。

Netty的ChannelFuture

public Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {
    checkNotNull(listener, "listener");
    synchronized (this) {
        addListener0(listener);
    }
    if (isDone()) {
        notifyListeners();
    }
    return this;
}
private boolean setValue0(Object objResult) {
    if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
        RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
        if (checkNotifyWaiters()) {
            notifyListeners();
        }
        return true;
    }
    return false;
}

通過addListener方法註冊監聽。如果任務完成,會呼叫notifyListeners通知。

CompletableFuture通過擴充套件Future,引入函數語言程式設計,通過回撥的方式去處理結果。

3.功能

CompletableFuture的功能主要體現在他的CompletionStage。

可以實現如下等功能

  • 轉換(thenCompose)
  • 組合(thenCombine)
  • 消費(thenAccept)
  • 執行(thenRun)。
  • 帶返回的消費(thenApply)
    消費和執行的區別:
    消費使用執行結果。執行則只是執行特定任務。具體其他功能大家可以根據需求自行檢視。

CompletableFuture藉助CompletionStage的方法可以實現鏈式呼叫。並且可以選擇同步或者非同步兩種方式。

這裡舉個簡單的例子來體驗一下他的功能。

public static void thenApply() {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    CompletableFuture cf = CompletableFuture.supplyAsync(() -> {
        try {
            //  Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("supplyAsync " + Thread.currentThread().getName());
        return "hello";
    }, executorService).thenApplyAsync(s -> {
        System.out.println(s + "world");
        return "hhh";
    }, executorService);
    cf.thenRunAsync(() -> {
        System.out.println("ddddd");
    });
    cf.thenRun(() -> {
        System.out.println("ddddsd");
    });
    cf.thenRun(() -> {
        System.out.println(Thread.currentThread());
        System.out.println("dddaewdd");
    });
}

執行結果

supplyAsync pool-1-thread-1
helloworld
ddddd
ddddsd
Thread[main,5,main]
dddaewdd

根據結果我們可以看到會有序執行對應任務。

注意:

如果是同步執行cf.thenRun。他的執行執行緒可能main執行緒,也可能是執行源任務的執行緒。如果執行源任務的執行緒在main呼叫之前執行完了任務。那麼cf.thenRun方法會由main執行緒呼叫。

這裡說明一下,如果是同一任務的依賴任務有多個:

如果這些依賴任務都是同步執行。那麼假如這些任務被當前呼叫執行緒(main)執行,則是有序執行,假如被執行源任務的執行緒執行,那麼會是倒序執行。因為內部任務資料結構為LIFO。
如果這些依賴任務都是非同步執行,那麼他會通過非同步執行緒池去執行任務。不能保證任務的執行順序。
上面的結論是通過閱讀原始碼得到的。下面我們深入原始碼。

3.原始碼追蹤
建立CompletableFuture
建立的方法有很多,甚至可以直接new一個。我們來看一下supplyAsync非同步建立的方法。

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                   Executor executor) {
    return asyncSupplyStage(screenExecutor(executor), supplier);
}
static Executor screenExecutor(Executor e) {
    if (!useCommonPool && e == ForkJoinPool.commonPool())
        return asyncPool;
    if (e == null) throw new NullPointerException();
    return e;
}

入參Supplier,帶返回值的函式。如果是非同步方法,並且傳遞了執行器,那麼會使用傳入的執行器去執行任務。否則採用公共的ForkJoin並行執行緒池,如果不支援並行,新建一個執行緒去執行。

這裡我們需要注意ForkJoin是通過守護執行緒去執行任務的。所以必須有非守護執行緒的存在才行。

asyncSupplyStage方法

static <U> CompletableFuture<U> asyncSupplyStage(Executor e,
                                                 Supplier<U> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<U> d = new CompletableFuture<U>();
    e.execute(new AsyncSupply<U>(d, f));
    return d;
}

這裡會建立一個用於返回的CompletableFuture。

然後構造一個AsyncSupply,並將建立的CompletableFuture作為構造引數傳入。
那麼,任務的執行完全依賴AsyncSupply。

AsyncSupply#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();
    }
}

1.該方法會呼叫Supplier的get方法。並將結果設定到CompletableFuture中。我們應該清楚這些操作都是在非同步執行緒中呼叫的。

2.d.postComplete方法就是通知任務執行完成。觸發後續依賴任務的執行,也就是實現CompletionStage的關鍵點。
在看postComplete方法之前我們先來看一下建立依賴任務的邏輯。

thenAcceptAsync方法

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(asyncPool, action);
}
private CompletableFuture<Void> uniAcceptStage(Executor e,
                                               Consumer<? super T> f) {
    if (f == null) throw new NullPointerException();
    CompletableFuture<Void> d = new CompletableFuture<Void>();
    if (e != null || !d.uniAccept(this, f, null)) {
        # 1
        UniAccept<T> c = new UniAccept<T>(e, d, this, f);
        push(c);
        c.tryFire(SYNC);
    }
    return d;
}

上面提到過。thenAcceptAsync是用來消費CompletableFuture的。該方法呼叫uniAcceptStage。

uniAcceptStage邏輯:

1.構造一個CompletableFuture,主要是為了鏈式呼叫。

2.如果為非同步任務,直接返回。因為源任務結束後會觸發非同步執行緒執行對應邏輯。

3.如果為同步任務(e==null),會呼叫d.uniAccept方法。這個方法在這裡邏輯:如果源任務完成,呼叫f,返回true。否則進入if程式碼塊(Mark 1)。

4.如果是非同步任務直接進入if(Mark 1)。

Mark1邏輯:

1.構造一個UniAccept,將其push入棧。這裡通過CAS實現樂觀鎖實現。

2.呼叫c.tryFire方法。

final CompletableFuture<Void> tryFire(int mode) {
    CompletableFuture<Void> d; CompletableFuture<T> a;
    if ((d = dep) == null ||
        !d.uniAccept(a = src, fn, mode > 0 ? null : this))
        return null;
    dep = null; src = null; fn = null;
    return d.postFire(a, mode);
}

1.會呼叫d.uniAccept方法。其實該方法判斷源任務是否完成,如果完成則執行依賴任務,否則返回false。

2.如果依賴任務已經執行,呼叫d.postFire,主要就是Fire的後續處理。根據不同模式邏輯不同。
這裡簡單說一下,其實mode有同步非同步,和迭代。迭代為了避免無限遞迴。

這裡強調一下d.uniAccept方法的第三個引數。

如果是非同步呼叫(mode>0),傳入null。否則傳入this。
區別看下面程式碼。c不為null會呼叫c.claim方法。

try {
    if (c != null && !c.claim())
        return false;
    @SuppressWarnings("unchecked") S s = (S) r;
    f.accept(s);
    completeNull();
} catch (Throwable ex) {
    completeThrowable(ex);
}

final boolean claim() {
    Executor e = executor;
    if (compareAndSetForkJoinTaskTag((short)0, (short)1)) {
        if (e == null)
            return true;
        executor = null; // disable
        e.execute(this);
    }
    return false;
}

claim方法是邏輯:

如果非同步執行緒為null。說明同步,那麼直接返回true。最後上層函式會呼叫f.accept(s)同步執行任務。
如果非同步執行緒不為null,那麼使用非同步執行緒去執行this。
this的run任務如下。也就是在非同步執行緒同步呼叫tryFire方法。達到其被非同步執行緒執行的目的。

public final void run()                { tryFire(ASYNC); }

看完上面的邏輯,我們基本理解依賴任務的邏輯。

其實就是先判斷源任務是否完成,如果完成,直接在對應執行緒執行以來任務(如果是同步,則在當前執行緒處理,否則在非同步執行緒處理)

如果任務沒有完成,直接返回,因為等任務完成之後會通過postComplete去觸發呼叫依賴任務。

postComplete方法

final void postComplete() {
    /*
     * On each step, variable f holds current dependents to pop
     * and run.  It is extended along only one path at a time,
     * pushing others to avoid unbounded recursion.
     */
    CompletableFuture<?> f = this; Completion h;
    while ((h = f.stack) != null ||
           (f != this && (h = (f = this).stack) != null)) {
        CompletableFuture<?> d; Completion t;
        if (f.casStack(h, t = h.next)) {
            if (t != null) {
                if (f != this) {
                    pushStack(h);
                    continue;
                }
                h.next = null;    // detach
            }
            f = (d = h.tryFire(NESTED)) == null ? this : d;
        }
    }
}

在源任務完成之後會呼叫。

其實邏輯很簡單,就是迭代堆疊的依賴任務。呼叫h.tryFire方法。NESTED就是為了避免遞迴死迴圈。因為FirePost會呼叫postComplete。如果是NESTED,則不呼叫。

堆疊的內容其實就是在依賴任務建立的時候加入進去的。上面我們已經提到過。

4.總結

基本上述原始碼已經分析了邏輯。

因為涉及非同步等操作,我們需要理一下(這裡針對全非同步任務):

1.建立CompletableFuture成功之後會通過非同步執行緒去執行對應任務。

2.如果CompletableFuture還有依賴任務(非同步),會將任務加入到CompletableFuture的堆疊儲存起來。以供後續完成後執行依賴任務。

當然,建立依賴任務並不只是將其加入堆疊。如果源任務在建立依賴任務的時候已經執行完成,那麼當前執行緒會觸發依賴任務的非同步執行緒直接處理依賴任務。並且會告訴堆疊其他的依賴任務源任務已經完成。

主要是考慮程式碼的複用。所以邏輯相對難理解。

postComplete方法會被源任務執行緒執行完源任務後呼叫。同樣也可能被依賴任務執行緒後呼叫。

執行依賴任務的方法主要就是靠tryFire方法。因為這個方法可能會被多種不同型別執行緒觸發,所以邏輯也繞一點。(其他依賴任務執行緒、源任務執行緒、當前依賴任務執行緒)

如果是當前依賴任務執行緒,那麼會執行依賴任務,並且會通知其他依賴任務。
如果是源任務執行緒,和其他依賴任務執行緒,則將任務轉換給依賴執行緒去執行。不需要通知其他依賴任務,避免死遞迴。

不得不說Doug Lea的編碼,真的是藝術。程式碼的複用性全體現在邏輯上了。

連結:https://blog.csdn.net/weixin_...

最後,給大家分享我收藏的幾個不錯的 github 專案,內容都還是不錯的,如果覺得有幫助,可以順便給個 star。

相關文章