【設計模式】非同步阻塞、非同步回撥模式

酷酷-發表於2024-10-30

1 前言

為什麼要看這個非同步回撥呢?是因為我上節在看 RocektMQ 傳送訊息的時候,它支援同步、非同步、一次性的模式,後兩者不會阻塞當前執行緒,但是看這兩者都沒用到執行緒池,那它是如何處理的呢?我們看下三者最後的落點,都是在 NettyRemotingAbstract 這個類裡:

// NettyRemotingAbstract#invokeSyncImpl
public RemotingCommand invokeSyncImpl(Channel channel, RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
    RemotingCommand var9;
    try {
        ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, (InvokeCallback)null, (SemaphoreReleaseOnlyOnce)null);
        this.responseTable.put(opaque, responseFuture);
        SocketAddress addr = channel.remoteAddress();
        // 設定回撥監聽
        channel.writeAndFlush(request).addListener((f) -> {
            if (f.isSuccess()) {
                responseFuture.setSendRequestOK(true);
            } else {
                responseFuture.setSendRequestOK(false);
                this.responseTable.remove(opaque);
                responseFuture.setCause(f.cause());
                // 觸發 countDownLatch.countDown();
                responseFuture.putResponse((RemotingCommand)null);
                log.warn("Failed to write a request command to {}, caused by underlying I/O operation failure", addr);
            }
        });
        // 阻塞當前執行緒 countDownLatch.await(3秒)
        RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
        if (null == responseCommand) {
            if (responseFuture.isSendRequestOK()) {
                throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis, responseFuture.getCause());
            }

            throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
        }

        var9 = responseCommand;
    } finally {
        this.responseTable.remove(opaque);
    }

    return var9;
}
// NettyRemotingAbstract#invokeAsyncImpl
public void invokeAsyncImpl(Channel channel, RemotingCommand request, long timeoutMillis, InvokeCallback invokeCallback) throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
    if (acquired) {
        SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
        long costTime = System.currentTimeMillis() - beginStartTime;
        if (timeoutMillis < costTime) {
            once.release();
            throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
        } else {
            ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
            this.responseTable.put(opaque, responseFuture);

            try {
                // 設定回撥監聽
                channel.writeAndFlush(request).addListener((f) -> {
                    if (f.isSuccess()) {
                        responseFuture.setSendRequestOK(true);
                    } else {
                        this.requestFail(opaque);
                        log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
                    }
                });
            } catch (Exception var15) {
                this.responseTable.remove(opaque);
                responseFuture.release();
                log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", var15);
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), var15);
            }
        }
    } 
    ...
}
// NettyRemotingAbstract#invokeOnewayImpl
public void invokeOnewayImpl(Channel channel, RemotingCommand request, long timeoutMillis) throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
    ...
    try {
        // 設定回撥監聽
        channel.writeAndFlush(request).addListener((f) -> {
            once.release();
            if (!f.isSuccess()) {
                log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed.");
            }

        });
    } catch (Exception var8) {
        ...
    }
    ...
}

可以看到三種模式的處理,一次性以及非同步的處理是一樣的,都是新增上回撥監聽即可,channel.writeAndFlush(request) 返回的是 Netty 裡的 ChannelFuture ,當非同步處理完讀寫然後執行你的回撥。而同步的裡邊,設定上監聽,並且自己透過 CountDownLauch 來阻塞當前執行緒,當非同步的回撥裡執行完成,countDown()後,當前執行緒才繼續往下走。這裡就用到了 Netty 裡的非同步回撥,所以我們本節就看看非同步回撥以及跟他相關的非同步阻塞。

2 基本概念

非同步回撥相對應的還有一個非同步阻塞,非同步阻塞屬於主動模式的非同步呼叫;非同步回撥屬於被動模式的非同步呼叫。

主動呼叫是一種阻塞式呼叫,“呼叫方”要等待“被呼叫方”執行完畢才返回。如果“被調 用方”的執行時間很長,那麼“呼叫方”執行緒需要阻塞很長一段時間。而被動的呼叫模式,也就是說,被呼叫方在執行完成後,會反向執行“呼叫方”所設定的鉤子方法。

主動呼叫比如我們熟悉的 join、FutureTask.get 都是會阻塞當前執行緒,非同步回撥比如 CompletableFuture、谷歌的 Guava Future相關技術、Netty的非同步回撥技術。

接下來我們就分別看下非同步阻塞以及非同步回撥的一些實現。

3 非同步阻塞

3.1 Join

join操作的原理是阻塞當前的執行緒,直到待合併的目標執行緒的執行完成。比如使用join()實現泡茶喝是一個非同步阻塞版本,具體的程式碼實現如下:

public class JustDemo {
    @SneakyThrows
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "在做事");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });

        thread.start();
        // 等待 thread 做完事
        thread.join();
        // main 其他事
        System.out.println("main 去做其他事情了");

    }
}

程式中有兩個執行緒:thread執行緒和main執行緒,main執行緒呼叫了 thread 的 join 方法,相當於合併了 thread 執行緒,等 thread 執行緒執行完,自己才能繼續執行其他事情。

關於 join 方法的詳解可以參考我這篇:【執行緒基礎】【四】join()方法詳解

join()方法應用場景:A執行緒呼叫B執行緒的join()方法,等待B執行緒執行完成;在B執行緒沒有完成前,A執行緒阻塞。

join()有一個問題:被合併執行緒沒有返回值。join執行緒合併就像一個悶葫蘆。只能發起合併執行緒,不能取到執行結果。並且它的等待機制如下:

public final synchronized void join(long millis)throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

join()的實現原理是不停地檢查join執行緒是否存活,如果join執行緒存活,wait(0)就永遠等下去, 直至join執行緒終止後,執行緒的this.notifyAll()方法會被呼叫(該方法是在JVM中實現的,JDK中並不 會看到原始碼),join()方法將退出迴圈,恢復主執行緒執行。很顯然這種迴圈檢查的方式比較低效。

除此之外,呼叫join()缺少很多靈活性,比如實際專案中很少讓自己單獨建立執行緒,而是使用 Executor,這進一步減少了join()的使用場景,所以join()的使用多數停留在Demo演示上。

3.2 FutureTask

如果需要獲得非同步執行緒的執行結果,怎麼辦呢?可以使用Java的FutureTask系列類。那我們上邊的例子可以這麼寫:

public class JustDemo {
    @SneakyThrows
    public static void main(String[] args) {
        FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName() + "在做事");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            return Boolean.TRUE;
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        Boolean result = futureTask.get();
        System.out.println(result);
        // main 其他事
        System.out.println("main 去做其他事情了");

    }
}

關於 FutureTask 的原理,這裡就不復述了可以參考我的這篇:【Java 執行緒池】【六】執行緒池submit、Future、FutureTask原理

透過FutureTask例項取得非同步執行緒的執行結果。一般來說,透過FutureTask例項的get() 方法可以獲取執行緒的執行結果。總之,FutureTask比join執行緒合併操作更加高明,能取得非同步執行緒的結果。但是,也就未必高 明到哪裡去。為什麼呢?

因為透過FutureTask的get()方法獲取非同步結果時,主執行緒也會被阻塞。這一點FutureTask和join是一致的,它們都是非同步阻塞模式。那麼接下來我們看看非同步回撥。

4 非同步回撥

4.1 CompletableFuture

Java8提供一個新的、具備非同步回撥能力的工具類 ——CompletableFuture,它是JDK 1.8引入的實現類,該類實現了Future和CompletionStage兩個介面。該 類的例項作為一個非同步任務,可以在自己非同步執行完成之後觸發一些其他的非同步任務,從而達到非同步回撥的效果。

CompletableFuture的UML類關係圖如下:

對於Future介面,大家已經非常熟悉了,接下來介紹一下CompletionStage介面。CompletionStage 代表非同步計算過程中的某一個階段,一個階段完成以後可能會進入另一個階段。一個階段可以理解 為一個子任務,每個子任務會包裝一個Java函式式介面例項,表示該子任務所要執行的操作。

CompletionStage代表某個同步或者非同步計算的一個階段或者是一系列非同步任務中的一個子任務(或者階段性任務)。

每個CompletionStage子任務所包裝的可以是一個Function、Consumer或者Runnable函式式介面 例項。這三個常用的函式式介面的特點如下:

(1)Function

Function介面的特點是:有輸入、有輸出。包裝了Function例項的CompletionStage子任務需要一個輸入引數,並會產生一個輸出結果到下一步。
(2)Runnable

Runnable介面的特點是:無輸入、無輸出。包裝了Runnable例項的CompletionStage子任務既不需要任何輸入引數,又不會產生任何輸出。
(3)Consumer

Consumer介面的特點是:有輸入、無輸出。包裝了Consumer例項的CompletionStage子任務需要一個輸入引數,但不會產生任何輸出。多個CompletionStage構成了一條任務流水線,一個環節執行完成了就可以將結果移交給下一個環節(子任務)。多個CompletionStage子任務之間可以使用鏈式呼叫,下面是一個簡單的例子:

oneStage.thenApply(x-> square(x))
 .thenAccept(y-> System.out.println(y))
 .thenRun(()-> System.out.println())

對以上例子中的CompletionStage子任務說明如下:

(1)oneStage是一個CompletionStage子任務,這是一個前提。

(2)“x->square(x)”是一個Function型別的Lambda表示式,被thenApply方法包裝成了一個CompletionStage子任務,該子任務需要接收一個引數x,然後會輸出一個結果——x的平方值。

(3)“y->System.out.println(y)”是一個Consumer型別的Lambda表示式,被thenAccept()方法包裝成了一個CompletionStage子任務,該子任務需要上一個子任務的輸出值,但是此子任務並沒有輸出。

(4)“()->System.out.println()”是一個Runnable型別的Lambda表示式,被thenRun()方法包裝成了一個CompletionStage子任務,既不需要上一個子任務的輸出值,又不產生結果。

CompletionStage代表非同步計算過程中的某一個階段,一個階段完成以後可能會觸發另一個階段。雖然一個子任務可以觸發其他子任務,但是並不能保證後續子任務的執行順序。

4.1.1 使用runAsync和supplyAsync 建立子任務

CompletionStage子任務的建立是透過CompletableFuture完成的。CompletableFuture類提供了非常強大的Future的擴充套件功能來幫助我們減少非同步程式設計的複雜性,提供了函數語言程式設計的能力來幫助我們透過回撥的方式處理計算結果,也提供了轉換和組合CompletionStage()的方法。CompletableFuture定義了一組方法用於建立CompletionStage子任務(或者階段性任務),基礎的方法如下:

 //子任務包裝一個Runnable例項,並使用ForkJoinPool.commonPool()執行緒池來執行
public static CompletableFuture<Void> runAsync(Runnable runnable)
 //子任務包裝一個Runnable例項,並呼叫指定的executor執行緒池來執行
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
 //子任務包裝一個Supplier例項,並呼叫ForkJoinPool.commonPool()執行緒池來執行
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
 //子任務包裝一個Supplier例項,並呼叫指定的executor執行緒池來執行
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

在使用CompletableFuture建立CompletionStage子任務時,如果沒有指定Executor執行緒池,在預設情況下CompletionStage會使用公共的ForkJoinPool執行緒池

那麼我們採用 CompletableFuture 的方式上邊的例子,我們可以這麼寫:

public class JustDemo {
    @SneakyThrows
    public static void main(String[] args) {
        CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "在做事");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            return Boolean.TRUE;
        });
        Boolean result = future.get();
        System.out.println(result);
        // main 其他事
        System.out.println("main 去做其他事情了");
    }
}

4.1.2 設定的子任務回撥鉤子

可以為CompletionStage子任務設定特定的回撥鉤子,當計算結果完成或者丟擲異常的時候, 可以執行這些特定的回撥鉤子。

設定子任務回撥鉤子的主要函式如下:

//設定子任務完成時的回撥鉤子
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
 //設定子任務完成時的回撥鉤子,可能不在同一執行緒執行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
 //設定的子任務完成時的回撥鉤子,提交給執行緒池executor執行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
 //設定的異常處理的回撥鉤子
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)

那我們上邊的例子可以改寫成:

public class JustDemo {
    @SneakyThrows
    public static void main(String[] args) {
        CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName() + "在做事");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            return Boolean.TRUE;
        });
        // 設定執行完畢的回撥
        future.whenComplete((res, e) -> {
            System.out.println(res);
            // main 其他事
            System.out.println("main 去做其他事情了");
        });
        // 因為預設的公告執行緒池裡的執行緒都是守護執行緒 所以我們這裡等待一下
        TimeUnit.SECONDS.sleep(2);
    }
}

CompletableFuture 還有很多的玩法,我們這裡就不一一列舉,這裡主要體現一下它的回撥設定哈、

4.2 Guava Future

Guava是Google提供的Java擴充套件包,它提供了一種非同步回撥的解決方案。Guava中與非同步回撥相關的原始碼處於com.google.common.util.concurrent包中。包中的很多類都用於對java.util.concurrent的能力擴充套件和能力增強。比如,Guava的非同步任務介面ListenableFuture擴充套件了Java的Future介面,實現了非同步回撥的的能力。

Guava主要增強了Java而不是另起爐灶。為了實現非同步回撥方式獲取非同步執行緒的結果,Guava做了以下增強:

引入了一個新的介面ListenableFuture,繼承了Java的Future介面,使得Java的Future非同步任務在Guava中能被監控和以非阻塞方式獲取非同步結果。

引入了一個新的介面FutureCallback, 這是一個獨立的新介面。 該介面的目的是在非同步任務執行完成後,根據非同步結果完成不同的回撥處理,並且可以處理非同步結果。

FutureCallback是一個新增的介面,用來填寫非同步任務執行完後的監聽邏輯。FutureCallback擁有兩個回撥方法:

public interface FutureCallback<V> {
  // 在非同步任務執行成功後被回撥。呼叫時,非同步任務的執行結果作為onSuccess()方法的引數被傳入
  void onSuccess(@Nullable V result);

  // 在非同步任務執行過程中丟擲異常時被回撥。呼叫時,非同步任務所丟擲的異常作為onFailure方法的引數被傳入
  void onFailure(Throwable t);
}

回撥任務怎麼跟我們的非同步任務關聯呢?Guava引入了一個新介面ListenableFuture,它繼承了Java的Future介面。

Guava的ListenableFuture介面是對Java的Future介面的擴充套件, 可以理解為非同步任務例項:

public interface ListenableFuture<V> extends Future<V> {
  void addListener(Runnable listener, Executor executor);
}

ListenableFuture僅僅增加了一個addListener()方法。它的作用就是將9.5.1節的FutureCallback善後 回 調 邏 輯 封 裝 成 一 個 內 部 的 Runnable 異 步 回 調 任 務 , 在 Callable 異 步 任 務 完 成 後 回 調FutureCallback善後邏輯。

注意,此addListener()方法只在Guava內部使用,如果對它感興趣,可以檢視Guava原始碼。在實際程式設計中,addListener()不會使用到。

在實際程式設計中,如何將FutureCallback回撥邏輯繫結到非同步的ListenableFuture任務呢?可以使用Guava的Futures工具類,它有一個addCallback()靜態方法,可以將FutureCallback的回撥例項繫結到ListenableFuture非同步任務。下面是一個簡單的繫結例項:

Futures.addCallback(listenableFuture, new FutureCallback<Boolean>() {
    public void onSuccess(Boolean r) {
        // listenableFuture內部的Callable成功時回撥此方法
    }
    public void onFailure(Throwable t) {
        // listenableFuture內部的Callable異常時回撥此方法
    }
}, executors);

現在的問題來了, 既然Guava的ListenableFuture介面是對Java的Future介面的擴充套件, 兩者都表示非同步任務,那麼Guava的非同步任務例項從何而來?

如果要獲取Guava的ListenableFuture非同步任務例項,主要是透過向執行緒池(ThreadPool)提交Callable任務的方式獲取。不過,這裡所說的執行緒池不是Java的執行緒池,而是經過Guava自己定製過的Guava執行緒池。

Guava執行緒池是對Java執行緒池的一種裝飾。建立Guava執行緒池的方法如下:

// Java執行緒池
ExecutorService jPool = Executors.newFixedThreadPool(10);
// Guava執行緒池
ListeningExecutorService gPool = MoreExecutors.listeningDecorator(jPool);

首先建立Java執行緒池, 然後以其作為Guava執行緒池的引數再構造一個Guava執行緒池。 有了Guava的執行緒池之後,就可以透過submit()方法來提交任務了,任務提交之後的返回結果就是我們所要的ListenableFuture非同步任務例項。

簡單來說, 獲取非同步任務例項的方式是透過向執行緒池提交Callable業務邏輯來實現, 程式碼如下:

// submit()方法用來提交任務,返回非同步任務例項
ListenableFuture<Boolean> hFuture = gPool.submit(hJob);
// 繫結回撥例項
Futures.addCallback(listenableFuture, new FutureCallback<Boolean>(){
    // 有兩種實現回撥的方法
}, gPool);

取到了ListenableFuture例項後, 透過Futures.addCallback()方法將FutureCallback回撥邏輯的例項繫結到ListenableFuture非同步任務例項,實現非同步執行完成後的回撥。

總結一下,Guava非同步回撥的流程如下:
(1)實現Java的Callable介面,建立的非同步執行邏輯。還有一種情況,如果不需要返回值,非同步執行邏輯也可以實現Runnable介面。
(2)建立Guava執行緒池。
(3)將 (1)建立的 Callable/Runnable 非同步執行邏輯的例項提交到 Guava 執行緒池 , 從而獲取ListenableFuture非同步任務例項。
(4)建立FutureCallback回撥例項,透過Futures.addCallback將回撥例項繫結到ListenableFuture非同步任務上。

完成以上4步, 當Callable/Runnable非同步執行邏輯完成後, 就會回撥非同步回撥例項FutureCallback例項的回撥方法onSuccess()/onFailure()。

接下來我們再改一下上邊的例子:

public class JustDemo {
    @SneakyThrows
    public static void main(String[] args) {
        // 建立 Java 裡的執行緒池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        // 用 Guava 將執行緒池包裝一層
        ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(executorService);
        // 提交任務
        ListenableFuture<Boolean> listenableFuture = listeningExecutorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "在做事");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            return Boolean.TRUE;
        });
        // 設定回撥
        Futures.addCallback(listenableFuture, new FutureCallback<Boolean>() {
            @Override
            public void onSuccess(@Nullable Boolean result) {
                if (Objects.nonNull(result) && result) {
                    System.out.println("回掉到我了 去做其他事情了");
                }
            }
            @Override
            public void onFailure(Throwable t) {
                System.out.println("失敗了,,msg=" + t.getMessage());
            }
        }, listeningExecutorService);

        System.out.println("main 繼續");
    }
}

截圖跟上邊的不一樣,是因為晚上回家在 mac上執行的哈:

總結一下Guava非同步回撥和Java的FutureTask非同步呼叫的區別,具體如下:

(1)FutureTask是主動呼叫的模式,“呼叫執行緒”主動獲得非同步結果,在獲取非同步結果時處於阻塞狀態,並且會一直阻塞,直到拿到非同步執行緒的結果。

(2)Guava是非同步回撥模式,“呼叫執行緒”不會主動去獲得非同步結果,而是準備好回撥函式,並設定好回撥鉤子; 執行回撥函式的並不是 “呼叫執行緒” 自身, 回撥函式的執行者是 “被呼叫執行緒”,“呼叫執行緒” 在執行完自己的業務邏輯後就已經結束了。當回撥函式被執行時,“呼叫執行緒” 已經結束很久了。

4.3 Netty Future

Netty官方文件說明Netty的網路操作都是非同步的。Netty原始碼中大量使用了非同步回撥處理模式。在Netty的業務開發層面,處於Netty應用的Handler處理程式中的業務處理程式碼也都是非同步執行的。所以,瞭解Netty的非同步回撥,無論是Netty應用開始還是原始碼級開發都是十分重要的。

Netty和Guava一樣, 實現了自己的非同步回撥體系: Netty繼承和擴充套件了JDK Future系列非同步回撥的API,定義了自身的Future系列介面和類,實現非同步任務的監控、非同步執行結果的獲取。

總的來說,Netty對Java Future非同步任務的擴充套件如下:

繼承Java的Future介面得到一個新的屬於Netty自己的Future非同步任務介面;該介面對原有的介面進行了增強,使得Netty非同步任務能夠非阻塞地處理回撥結果。注意,Netty沒有修改Future的名稱,只是調整了所在的包名,Netty的Future類的包名和Java的Future介面的包不同。

引入了一個新介面——GenericFutureListener,用於表示非同步執行完成的監聽器。這個介面和Guava的FutureCallbak回撥介面不同。Netty使用了監聽器的模式,非同步任務執行完成後的回撥邏輯抽象成了Listener監聽器介面。可以將Netty的GenericFutureListener監聽器介面加入Netty非同步任務Future中,實現對非同步任務執行狀態的事件監聽。

總的來說,在非同步非阻塞回撥的設計思路上,Netty和Guava是一致的。對應關係為:

(1)Netty的Future介面可以對應到Guava的ListenableFuture介面

(2)Netty的GenericFutureListener介面可以對應到Guava的FutrueCallback介面

GenericFutureListener位於io.netty.util.concurrent包中,原始碼如下:

public interface GenericFutureListener<F extends Future<?>> extends EventListener {
    void operationComplete(F var1) throws Exception;
}

GenericFutureListener擁有一個回撥方法operationComplete(), 表示非同步任務操作完成。 在Future非同步任務執行完成後將回撥此方法 。 大 多 數 情 況 下 , Netty 的 異 步 回 調 的 代 碼 編 寫 在GenericFutureListener介面的實現類中的operationComplete()方法中。

說明一下, GenericFutureListener的父介面EventListener是一個空介面,沒有任何抽象方法,是一個僅僅具有標識作用的介面。

Netty也對Java的Future介面進行了擴充套件,並且名稱沒有變,還是被稱為Future介面,實現在io.netty.util.concurrent包中。和Guava的ListenableFuture一樣,Netty的Future介面擴充套件了一系列方法,對執行的過程進行監控,對非同步回撥完成事件進行Listen監聽並且回撥。Netty的Future的原始碼如下:

public interface Future<V> extends java.util.concurrent.Future<V> {
    // 判斷非同步執行是否成功
    boolean isSuccess();
    // 判斷非同步執行是否取消
    boolean isCancellable();
    // 獲取非同步任務異常的原因
    Throwable cause();
    // 增加非同步任務執行完成Listener監聽器
    Future<V> addListener(GenericFutureListener<? extends Future<? super V>> var1);

    Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... var1);
    // 移除非同步任務執行完成Listener監聽器
    Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> var1);

    Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... var1);

    ...
}

Netty的Future介面一般不會直接使用,使用過程中會使用其他的子介面。Netty有一系列的子介面,代表不同型別的非同步任務,如ChannelFuture介面。

ChannelFuture子介面表示Channel通道I/O操作的非同步任務;如果在Channel的非同步I/O操作完成後,需要執行回撥操作,就需要使用到ChannelFuture介面,這個也是我們上邊看 RokcetMQ 傳送訊息的時候用到的非同步回撥機制。

在 Netty網 絡 編 程中 , 網 絡 連 接 通道 的 輸 入 、 輸 出處 理 都 是 異 步 進行 的 , 都 會 返 回一 個ChannelFuture介面的例項。 透過返回的非同步任務例項, 可以為其增加非同步回撥的監聽器。 在非同步任務真正完成後,回撥執行。

Netty的網路連線的非同步回撥,例項程式碼如下:

// connect是非同步的,僅僅是提交非同步任務
ChannelFuture future = bootstrap.connect(
    new InetSocketAddress("www.manning.com"
,80));
// connect的非同步任務真正執行完成後,future回撥監聽器會執行
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        if (channelFuture.isSuccess()){
            System.out.println("Connection established");
        } else {
            System.err.println("Connection attempt failed");
            channelFuture.cause().printStackTrace();
        }
    }
});

GenericFutureListener介面在Netty中是一個基礎型別介面。在網路程式設計的非同步回撥中,一般使用Netty中提供的某個子介面,如ChannelFutureListener介面。在上面的程式碼中,使用到的是這個子介面。

Netty的出站和入站操作都是非同步的。這裡非同步回撥的方法和前面Netty建立的非同步回撥是一樣的。在write操作呼叫後,Netty並沒有立即完成對Java NIO底層連線的寫入操作,底層的寫入操作是非同步執行的,程式碼如下:

// write()輸出方法,返回的是一個非同步任務
ChannelFuture future = ctx.channel().write(msg);
// 為非同步任務加上監聽器
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) {
        //write操作完成後的回撥程式碼
    }
});

在write操作完成後立即返回,返回的是一個ChannelFuture介面的例項。透過這個例項可以繫結非同步回撥監聽器,編寫非同步回撥的邏輯。

5 小結

好啦,本節我們從非同步阻塞的 Join、FutureTask 再到非同步回掉的 CompletableFuture、谷歌的 Guava Future相關技術、Netty的非同步回撥大致看了看,有理解不對的地方還請指正哈。

相關文章