使用 Java 8 的 CompletableFuture 實(shí)現(xiàn)函數(shù)式的回調(diào)

使用 Java 8 的 CompletableFuture 實(shí)現(xiàn)函數(shù)式的回調(diào)

最近,在準(zhǔn)備一個(gè)關(guān)于 Java 并行流相關(guān)的演講時(shí),我意識到“The Free Lunch is Over”(TFLiO)這篇經(jīng)典的文章已經(jīng)有超過十年的歷史了。對于大多數(shù)程序員來說,這篇文章的廣泛傳播使他們第一次認(rèn)識到持續(xù)四十年的處理器呈指數(shù)增長的趨勢將要終結(jié)——實(shí)際上,它已經(jīng)終結(jié)了。取而代之的是另外一種趨勢,那就是在每個(gè)芯片上增加處理器的數(shù)量,按照 Herb Sutter 的話來講,程序員必須要“從根本上轉(zhuǎn)向并發(fā)”。

在 TFLiO 這篇文章中,Sutter 觀察到絕大多數(shù)的程序員“并沒有深刻了解并發(fā)”,但是他接著說到“一旦了解之后,其實(shí)基于鎖的編程并不會比 OO 難太多”。毫無疑問,第一句話是完全正確的,但是十年來,基于鎖的并發(fā)體驗(yàn)并沒有證明第二句話的正確性。幸好,Java 程序員可以在很大程度上避免這樣的驗(yàn)證,因?yàn)?TFLiO 發(fā)布的時(shí)候,Java 5 也剛剛可用,它所提供的一些高層級的并發(fā)工具開始得到運(yùn)用。借助它們,能夠讓 Java 開發(fā)人員避免細(xì)粒度地分析同步功能和關(guān)鍵的代碼片段。

并發(fā)與并行

Java 5 并發(fā)庫主要關(guān)注于異步任務(wù)的處理,它采用了這樣一種模式,producer 線程創(chuàng)建任務(wù)并且利用阻塞隊(duì)列將其傳遞給任務(wù)的 consumer。這種模型在 Java 7 和 8 中進(jìn)一步發(fā)展,并且開始支持另外一種風(fēng)格的任務(wù)執(zhí)行,那就是將任務(wù)的數(shù)據(jù)集分解為子集,每個(gè)子集都可以由獨(dú)立且同質(zhì)的子任務(wù)來負(fù)責(zé)處理。

這種風(fēng)格的基礎(chǔ)庫也就是 fork/join 框架,它允許程序員規(guī)定數(shù)據(jù)集該如何進(jìn)行分割,并且支持將子任務(wù)提交到默認(rèn)的標(biāo)準(zhǔn)線程池中,也就是“通用的”ForkJoinPool。(在本文中,非全限定的類和接口名指的都是 java.util.concurrent 包中的類型。)在 Java 8 中,fork/join 并行功能借助并行流的機(jī)制變得更加具有可用性。但是,不是所有的問題都適合這種風(fēng)格的并行處理:所處理的元素必須是獨(dú)立的,數(shù)據(jù)集要足夠大,并且在并行加速方面,每個(gè)元素的處理成本要足夠高,這樣才能補(bǔ)償建立 fork/join 框架所消耗的成本。

同時(shí),Java 8 在并行流方面的革新得到了廣泛的關(guān)注,這導(dǎo)致大家忽略了并發(fā)庫中一項(xiàng)新增的重要功能,那就是CompletableFuture<T>類。本文將會探討CompletableFuture類,有一些系統(tǒng)會依賴于不同類型的異步執(zhí)行任務(wù),本文將會闡述該類為什么會對這種類型的系統(tǒng)如此重要,并介紹了它是如何補(bǔ)充 fork/join 風(fēng)格的并行機(jī)制和并行流的。

頁面渲染器

我們的起始點(diǎn)將是“Java Concurrency in Practice”(JCiP)一書中的樣例,這個(gè)樣例非常經(jīng)典地闡述了 Java 5 中的并發(fā)工具類。在 JCiP 的第 6.3 節(jié)中,Brian Goetz 探討了如何開發(fā)一個(gè) Web 頁面的渲染器,對于每個(gè)頁面來說,它的任務(wù)就是渲染文本,并下載和渲染圖片。圖片的下載會耗費(fèi)較長的時(shí)間,在這段時(shí)間內(nèi) CPU 無事可做,只能等待。所以,在渲染頁面時(shí),一個(gè)很明顯的策略就是首先初始化所有圖片的下載,然后利用它們完成之前的這段時(shí)間渲染頁面文本,最后渲染下載的圖片。

在 JCiP 中,第一版本的頁面渲染器使用了Future的理念,這個(gè)接口暴露了一些方法,允許客戶端監(jiān)控任務(wù)的執(zhí)行進(jìn)度,這里的任務(wù)是在一個(gè)不同的進(jìn)程中執(zhí)行的。在程序清單 1 中,Callable代表了下載頁面中所有圖片的任務(wù),它被提交到了 Executor 中,然后返回一個(gè) Future 對象,通過它就能詢問下載任務(wù)的狀態(tài)。當(dāng)主線程渲染完頁面的文本后,會調(diào)用Future.get方法,這個(gè)方法會一直阻塞直到所有下載的結(jié)果均可用為止,在本例中這個(gè)結(jié)果是以List<ImageData>的形式來表示的。這種方式一個(gè)很明顯的缺點(diǎn)在于下載任務(wù)的粗粒度性,在所有的圖片下載完成之前,我們一張圖片也不能渲染。接下來,我們看一下如何緩解這個(gè)問題。


public void renderPage(CharSequence source) {

List<ImageInfo> info = scanForImageInfo(source);

// 創(chuàng)建 Callable,它代表了下載所有的圖片

final Callable<List<ImageData>> task = () ->

  info.stream()

    .map(ImageInfo::downloadImage)

    .collect(Collectors.toList());

// 將下載任務(wù)提交到 executor

Future<List<ImageData>> images = executor.submit(task);

// renderText(source);

try {

  // 獲得所有下載的圖片(在所有圖片可用之前會一直阻塞)

  final List<ImageData> imageDatas = images.get();

  // 渲染圖片

  imageDatas.forEach(this::renderImage);

} catch (InterruptedException e) {

  // 重新維護(hù)線程的中斷狀態(tài)

  Thread.currentThread().interrupt();

  // 我們不需要結(jié)果,所以取消任務(wù)

  images.cancel(true);

} catch (ExecutionException e) {

  throw launderThrowable(e.getCause()); }

}

程序清單 1:使用 Future 等待所有的圖片下載完成

為了讓這個(gè)樣例及其后面的變種易于管理,這里有一個(gè)前提條件:我們假設(shè)類型ImageInfo(簡單來講,就是一個(gè) URL)和ImageData(圖片的二進(jìn)制數(shù)據(jù))以及方法scanForImageInfo、downloadImage、renderText、renderImage、launderThrowableImageInfo.downloadImage都已經(jīng)存在了。實(shí)例變量executor是通過ExecutorService類型聲明的并進(jìn)行了恰當(dāng)?shù)某跏蓟T诒疚闹校覍?JCiP 中最初的樣例利用 Java 8 lambda 表達(dá)式和流進(jìn)行了現(xiàn)代化。

在這段代碼中,必須要等待所有下載都完成的原因在于,它使用Future接口來代表下載任務(wù),作為異步執(zhí)行的任務(wù)模型,它有很大的局限性。Future允許客戶端詢問任務(wù)執(zhí)行的結(jié)果,如果必要的話,將會產(chǎn)生阻塞,另外還可以詢問任務(wù)的狀態(tài),判斷它已經(jīng)完成還是被取消了。但是,Future本身并不能提供回調(diào)方法,假設(shè)能夠這樣做的話,當(dāng)每個(gè)圖片下載完成的時(shí)候,就能通知頁面的渲染線程了。

程序清單 2 改善了之前樣例中粒度較粗的問題,它將頁面下載的任務(wù)提交到了CompletionService類中,這個(gè)類的polltake方法會產(chǎn)生對應(yīng)的Future實(shí)例,這些實(shí)例是按照任務(wù)完成的順序排列的,而不是像程序清單 1 那樣任務(wù)是按照提交的順序處理的。在ExecutorCompletionService接口的平臺實(shí)現(xiàn)中,為了實(shí)現(xiàn)該功能,每項(xiàng)任務(wù)都會包裝在一個(gè)FutureTask中,FutureTaskFuture的一個(gè)實(shí)現(xiàn),它允許提供完成時(shí)的回調(diào)。Future 的回調(diào)行為是在 ExecutorCompletionService 中創(chuàng)建的,完成的任務(wù)會封裝到一個(gè)隊(duì)列中,供客戶端詢問時(shí)使用。


public void renderPage(CharSequence source) {

  List<ImageInfo> info = scanForImageInfo(source);

  CompletionService<ImageData> completionService =

    new ExecutorCompletionService<>(executor);

  // 將每個(gè)下載任務(wù)提交到 completion service 中

  info.forEach(imageInfo ->

    completionService.submit(imageInfo::downloadImage));

  renderText(source);

  // 當(dāng)每個(gè) RunnableFuture 可用時(shí)(并且我們也準(zhǔn)備處理它的時(shí)候),

  // 將它們檢索出來

  for (int t = 0; t < info.size(); t++) {

    Future<ImageData> imageFuture = completionService.take();

    renderImage(imageFuture.get());

  }

}

程序清單 2:借助CompletionService,當(dāng)圖片可用時(shí)立即將其渲染出來(為了保持簡潔性,省略掉了中斷和錯(cuò)誤處理的代碼)

CompletableFuture 簡介

程序清單 2 代表了 Java 5 所能達(dá)到的水準(zhǔn),不過 2014 年之后,在 Java 中,編寫異步系統(tǒng)的表現(xiàn)性得到了巨大的提升,這是通過引入CompletableFuture (CF)類實(shí)現(xiàn)的。這個(gè)類是Future的實(shí)現(xiàn),它能夠?qū)⒒卣{(diào)放到與任務(wù)不同的線程中執(zhí)行,也能將回調(diào)作為繼續(xù)執(zhí)行的同步函數(shù),在與任務(wù)相同的線程中執(zhí)行。它避免了傳統(tǒng)回調(diào)最大的問題,那就是能夠?qū)⒖刂屏鞣蛛x到不同的事件處理器中,而這是通過允許CF實(shí)例與回調(diào)方法進(jìn)行組合形成新的CF來實(shí)現(xiàn)的。

作為樣例,可以參考thenAccept方法,它接受一個(gè)Consumer(用戶提供的且沒有返回值的函數(shù))并返回一個(gè)新的CF。這個(gè)新CF所能達(dá)到的效果就是在最初CF完成時(shí)所得到的結(jié)果上,運(yùn)用Consumer。與很多其他的CF方法類似,thenAccept有兩個(gè)變種形式,在第一個(gè)中,Consumer會由通用 fork/join 池中的某一個(gè)線程來執(zhí)行;在第二個(gè)中,它會由Executor中的某一個(gè)線程來負(fù)責(zé)執(zhí)行,而Executor是我們在調(diào)用時(shí)提供的。這形成了三種重載形式:同步運(yùn)行、在ForkJoinPool中異步運(yùn)行以及在調(diào)用時(shí)所提供的線程池中異步運(yùn)行,CompletableFuture中有近 60 個(gè)方法,上述的這三種重載形式占了絕大多數(shù)。

如下是thenAccept的一個(gè)樣例,借助它重新實(shí)現(xiàn)了頁面渲染器的功能:


public void renderPage(CharSequence source) {

        List<ImageInfo> info = scanForImageInfo(source);

        info.forEach(imageInfo ->

              CompletableFuture

      .supplyAsync(imageInfo::downloadImage)

      .thenAccept(this::renderImage));

        renderText(source);

}

程序清單 3:使用CompletableFuture來實(shí)現(xiàn)頁面渲染功能

盡管程序清單 3 比前面的形式更加簡潔,但是我們需要練習(xí)一下才能更好地閱讀它。工廠方法supplyAsync返回一個(gè)新的CF,它會在通用的ForkJoinPool中運(yùn)行指定的Supplier,完成時(shí),Supplier的結(jié)果將會作為CF的結(jié)果。方法thenAccept會返回一個(gè)新的CF,它將會執(zhí)行指定的Consumer,在本例中也就是渲染給定的圖片,即supplyAsync方法所產(chǎn)生的CF的結(jié)果。

需要澄清的是,thenAccept并不是將CF與函數(shù)組合起來的唯一方式。將CF與函數(shù)組合起來可以接受如下的參數(shù):

  • 應(yīng)用于CF操作結(jié)果的函數(shù)。此時(shí),可以采用的方法包括:

    • thenCompose:針對返回值為CompletableFuture的函數(shù);

    • thenApply:針對返回值為其他類型的函數(shù);

    • thenAccept:針對返回值為 void 的函數(shù);

  • Runnable。通過thenRun方法,可以接受Runnable參數(shù);

  • 函數(shù)在處理的過程中,可能正常結(jié)束也可能異常退出。CF能夠通過方法來分別組合這兩種情況:

    • handle,針對接受一個(gè)值和一個(gè) Throwable,并有返回值的函數(shù);

    • whenComplete,針對接受一個(gè)值和一個(gè) Throwable,并返回 void 的函數(shù)。

擴(kuò)展頁面渲染器

擴(kuò)展該樣例能夠闡述CompletableFuture的其他特性。比如,當(dāng)圖片下載超時(shí)或失敗時(shí),我們想使用一個(gè)圖標(biāo)作為可見的指示器。CF暴露了一個(gè)名為get(long, TimeUnit)的方法,如果CF在指定的時(shí)間內(nèi)沒有完成的話,將會拋出TimeoutException異常。我們可以使用它來定義一個(gè)函數(shù),這個(gè)函數(shù)會將ImageInfo轉(zhuǎn)換為ImageData (程序清單 4)。


Function<ImageInfo, ImageData> infoToData = imageInfo -> {

  CompletableFuture<ImageData> imageDataFuture =

      CompletableFuture.supplyAsync(imageInfo::downloadImage, executor);

  try {

      return imageDataFuture.get(5, TimeUnit.SECONDS);

  } catch (InterruptedException e) {

      Thread.currentThread().interrupt();

      imageDataFuture.cancel(true);

      return ImageData.createIcon(e);

  } catch (ExecutionException e) {

      throw launderThrowable(e.getCause());

  } catch (TimeoutException e) {

      return ImageData.createIcon(e);

  }

}

程序清單 4:使用CompletableFuture.get來實(shí)現(xiàn)超時(shí)

現(xiàn)在,頁面可以通過連續(xù)調(diào)用infoToData來進(jìn)行渲染。其中每個(gè)調(diào)用都會同步返回一個(gè)下載的圖片,所以要并行下載的話,需要為它們各自創(chuàng)建一個(gè)新的異步任務(wù)。要實(shí)現(xiàn)這一功能,合適的工廠方法是CompletableFuture.runAsync(),它與supplyAsync類似,但是接受的參數(shù)是Runnable而不是Supplier


public void renderPage(CharSequence source) throws InterruptedException {

      List<ImageInfo> info = scanForImageInfo(source);

      info.forEach(imageInfo ->

          CompletableFuture.runAsync(() ->

              renderImage(infoToData.apply(imageInfo)), executor));

}

現(xiàn)在,我們考慮進(jìn)一步的需求,當(dāng)所有的請求完成或超時(shí)后,在頁面上顯示一個(gè)指示器,如果對應(yīng)的所有CompletableFuture都從join方法中返回,就能表示出現(xiàn)了這種場景。靜態(tài)方法allOf就是為這種需求而提供的,它能夠創(chuàng)建一個(gè)返回值為空的CompletableFuture,當(dāng)其所有的組件均完成時(shí),它也會達(dá)到完成狀態(tài)。(join方法通常用來返回某個(gè)CF的結(jié)果,為了查看allOf方法所組合起來的所有CF的結(jié)果,必須要對其進(jìn)行單獨(dú)地查詢。)


public void renderPage(CharSequence source) {

      List<ImageInfo> info = scanForImageInfo(source);

      CompletableFuture[] cfs = info.stream()

          .map(ii -> CompletableFuture.runAsync(

              () -> renderImage(mapper.apply(ii)), executor))

          .toArray(CompletableFuture[]::new);

      CompletableFuture.allOf(cfs).join();

      renderImage(ImageData.createDoneIcon());

  }

聯(lián)合多個(gè) CompletableFuture

另外一組方法允許將多個(gè)CF聯(lián)合在一起。我們已經(jīng)看見過靜態(tài)方法allOf,當(dāng)其所有的組件均完成時(shí),它就會處于完成狀態(tài),與之對應(yīng)的方法也就是anyOf,返回值同樣是 void,當(dāng)其任意一個(gè)組件完成時(shí),它就會完成。除了這兩個(gè)方法以外,這個(gè)組中其他的方法都是實(shí)例方法,它們能夠?qū)?receiver 按照某種方式與另外一個(gè)CF聯(lián)合在一起,然后將結(jié)果傳遞到給定的函數(shù)中。

為了展現(xiàn)它們是如何運(yùn)行的,我們擴(kuò)展一下 JCiP 中的另一個(gè)例子,這是一個(gè)旅行預(yù)訂的門戶,我們將互相關(guān)聯(lián)的訂購過程記錄在TripPlan對象中,它包含了總價(jià)以及所使用服務(wù)供應(yīng)商的列表:


interface TripPlan {

      List<ServiceSupplier> getSuppliers();

      int getPrice();

      TripPlan combine(TripPlan);

  }

ServiceSupplier(比如說某個(gè)航線或酒店)能夠創(chuàng)建一個(gè)TripPlan:(當(dāng)然,在現(xiàn)實(shí)中,ServiceSupplier.createPlan 將會接受參數(shù),來反映對應(yīng)的目的地、旅行等級等信息。)


interface ServiceSupplier {

    TripPlan createPlan();

    String getAlliance();      // 稍后使用

}

為了選擇最佳的旅行計(jì)劃,需要查詢每個(gè)服務(wù)供應(yīng)商為我們的旅行所給定的規(guī)劃,然后使用Comparator來對比每個(gè)規(guī)劃結(jié)果,這個(gè)Comparator反映了我們的選擇標(biāo)準(zhǔn)(在本例中,只是簡單的選擇價(jià)格最低者):


TripPlan selectBestTripPlan(List<ServiceSupplier> serviceList) {

  List<CompletableFuture<TripPlan>> tripPlanFutures = serviceList.stream()

    .map(svc -> CompletableFuture.supplyAsync(svc::createPlan, executor))

    .collect(toList());

  return tripPlanFutures.stream()

    .min(Comparator.comparing(cf -> cf.join().getPrice()))

    .get().join();

}

請注意中間的collect操作,在流處理里面,由于中間操作的延遲性(laziness of intermediate operation),它就變得非常必要了。如果沒有它的話,流的終止操作(terminal operation)將會是min,它如果要執(zhí)行的話,首先需要針對tripPlanFutures的每個(gè)元素執(zhí)行join操作。如上述的代碼所示,我們并沒有這樣做,終止操作是collect,它會將map操作所形成的CF值累積起來,這個(gè)過程中沒有阻塞,因此允許底層的任務(wù)并發(fā)執(zhí)行。

如果獲取航線和酒店最佳旅行計(jì)劃的任務(wù)是獨(dú)立的,那么我們會希望它們能夠同時(shí)初始化,就像前文所述的圖片下載一樣。要將兩個(gè)CF按照這種方式聯(lián)合在一起,我們需要使用CompletableFuture.thenCombine方法,它會并行地執(zhí)行 receiver 以及所提供的CF,然后將它們的結(jié)果使用給定的函數(shù)組合起來(在這里,假設(shè)變量airlineshotels和(稍后使用的)cars都是以List<TravelService>類型進(jìn)行聲明的,并且已經(jīng)進(jìn)行了恰當(dāng)?shù)某跏蓟?/p>


CompletableFuture

      .supplyAsync(() -> selectBestTripPlan(airlines))

      .thenCombine(

          CompletableFuture.supplyAsync(() -> selectBestTripPlan(hotels)),

              TripPlan::combine);

對這個(gè)樣例進(jìn)行擴(kuò)展,我們將會學(xué)到更多的內(nèi)容。假設(shè)每個(gè)服務(wù)供應(yīng)商都屬于某一個(gè)旅行聯(lián)盟(travel alliance),通過String類型的屬性alliance來表示。在獨(dú)立訂購?fù)旰骄€和酒店后,我們將會確定它們是否屬于同一個(gè)聯(lián)盟,如果是的話,那么只有屬于同一聯(lián)盟的租車服務(wù),才在我們的考慮范圍之內(nèi):


  private TripPlan addCarHire(TripPlan p) {

      List<String> alliances = p.getSuppliers().stream()

          .map(ServiceSupplier::getAlliance)

          .distinct()

          .collect(toList());

      if (alliances.size() == 1) {

          return p.combine(selectBestTripPlan(cars, alliances.get(0)));

      } else {

          return p.combine(selectBestTripPlan(cars));

      }

  }

selectBestTripPlan方法新的重載形式將會接受一個(gè)String類型作為偏愛的聯(lián)盟,如果這個(gè)值存在的話,會使用它來過濾流中的服務(wù):


  private TripPlan selectBestTripPlan(

      List<ServiceSupplier> serviceList, String favoredAlliance) {

      List<CompletableFuture<TripPlan>> tripPlanFutures = serviceList.stream()

          .filter(ts ->

              favoredAlliance == null || ts.getAlliance().equals(favoredAlliance))

          .map(svc -> CompletableFuture.supplyAsync(svc::createPlan, executor))

          .collect(toList());

      ...

  }

在本例中,選擇租車服務(wù)的CF要依賴于航班和酒店預(yù)訂任務(wù)組合所形成的CF。只有航班和酒店都預(yù)訂之后,它才能完成。實(shí)現(xiàn)這種關(guān)聯(lián)關(guān)系的方法就是thenCompose


CompletableFuture.supplyAsync(() -> selectBestTripPlan(airlines))

      .thenCombine(

            CompletableFuture.supplyAsync(() -> selectBestTripPlan(hotels)),

                TripPlan::combine)

      .thenCompose(p -> CompletableFuture.supplyAsync(() -> addCarHire(p)));

預(yù)訂航班和酒店聯(lián)合形成的CF會執(zhí)行,并且它的結(jié)果,也就是聯(lián)合后的TripPlan,將會作為thenCompose函數(shù)參數(shù)的輸入。結(jié)果形成的CF非常簡潔地封裝了不同異步服務(wù)之間的依賴關(guān)系。這段代碼如此簡潔的原因在于,盡管thenCompose聯(lián)合了兩個(gè)CF,但是它所返回的并不是我們預(yù)期的CompletableFuture<CompletableFuture<TripPlan>>,而是CompletableFuture<TripPlan>。所以,不管在創(chuàng)建CF的時(shí)候使用了多少層級的組合,它并不是嵌套的,而是扁平的,要獲取它的結(jié)果只需要一步操作。這是 monad“綁定(bind)”操作(這個(gè)名稱來源于 Haskell)的特性,CF就是這種 monad,并且闡明了 monad 一些非常積極的特征:比如,在本例中,我們能夠按照函數(shù)式的形式進(jìn)行編寫,如果沒有這項(xiàng)功能的話,就需要在各個(gè)回調(diào)中非常繁瑣地顯式編寫任務(wù)定義。

thenCombine方法只是將兩個(gè)CF聯(lián)合起來的方法之一,其他的方法包括:

  • thenAcceptBoth:與thenCombine類似,但是它接受一個(gè)返回值為 void 的函數(shù);

  • runAfterBoth:接受一個(gè)Runnable,在兩個(gè)CF都完成后執(zhí)行;

  • applyToEither:接受一個(gè)一元函數(shù)(unary function),會將首先完成的 CF 的結(jié)果提供給它;

  • acceptEither:與 applyToEither 類似,接受一個(gè)一元函數(shù),但是結(jié)果為 void;

  • runAfterEither:接受一個(gè) Runnable,在其中一個(gè) CF 完成后就執(zhí)行。

結(jié)論

我們不可能在一篇短文中,完整地闡述像CompletableFuture這樣的 API,但是我希望這里的樣例能夠讓你對它所能實(shí)現(xiàn)的并發(fā)編程形式有一個(gè)直觀印象。將CompletableFuture與其他CompletableFuture組合,以及與其他函數(shù)組合,能夠?yàn)槎囗?xiàng)任務(wù)構(gòu)建類似流水線的方案,這樣能夠控制同步和異步執(zhí)行以及它們之間的依賴。你想更加詳細(xì)了解的內(nèi)容可能會包括異常處理、選擇和配置 executor 的實(shí)際經(jīng)驗(yàn)以及設(shè)計(jì)異步 API 所面臨的挑戰(zhàn)。

我希望已經(jīng)解釋清楚了 Java 8 所提供的兩種異步編程風(fēng)格之間的聯(lián)系。在使用 fork/join 并行機(jī)制(包括并行流)的場景中,能夠非常高效的將工作內(nèi)容進(jìn)行跨核心分發(fā)。但是,它的適用條件卻非常有限:數(shù)據(jù)集很大并且能夠高效地分割,對某個(gè)數(shù)據(jù)元素的操作與其他元素是(相對)獨(dú)立的,這些操作的成本應(yīng)該是比較高昂的,并且應(yīng)該是 CPU 密集型的。如果這些條件無法滿足的話,尤其是如果你的任務(wù)會花費(fèi)很多時(shí)間阻塞在 I/O 或網(wǎng)絡(luò)請求上的話,那么CompletableFuture是更好的替代方案。作為 Java 程序員,我們非常幸運(yùn)地有這樣一個(gè)平臺庫,它將這些補(bǔ)充的方式集成在了一起。

關(guān)于作者

Maurice Naftalin在軟件領(lǐng)域已經(jīng)工作了四十年,擔(dān)任過開發(fā)者、研究員、培訓(xùn)師以及圖書作者。他是一位 Java Champion,曾經(jīng)作為作者和合著者編寫過介紹 Java 5 和 Java 8 特性的圖書,并且是 2013 和 2014 年度的 JavaOne Rockstar。Maurice 對 Java Lambdas 的理解包含在了他的新書Mastering Lambdas - Java Programming in a Multicore World中。

查看英文原文:Functional-Style Callbacks Using Java 8's CompletableFuture

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。