Java 異步編程:從 Future 到 Loom
眾所周知,Java 開(kāi)始方法執(zhí)行到結(jié)束,都是由同一個(gè)線程完成的。這種方式雖易于開(kāi)發(fā)調(diào)試,但容易因?yàn)殒i、IO 等原因?qū)е戮€程掛起,產(chǎn)生線程上下文切換。隨著對(duì)應(yīng)用并發(fā)能力要求越來(lái)越高,頻繁的線程上下文切換所帶來(lái)的成本變得難以忽視。同時(shí),線程也是相對(duì)寶貴的資源,無(wú)限制的增加線程是不可能的。優(yōu)秀的技術(shù)人員應(yīng)該能讓應(yīng)用使用更少的線程資源實(shí)現(xiàn)更高的并發(fā)能力。這便是我們今天要討論的話題 —— Java 異步編程技術(shù)。
異步編程其實(shí)并沒(méi)有清晰定義。通常我們認(rèn)為,從方法開(kāi)始到結(jié)束都必須在同一個(gè)線程內(nèi)調(diào)度執(zhí)行的編程方式可被認(rèn)為是同步編程方式。但因?yàn)檫@樣的方式是我們習(xí)以為常的,所以也就沒(méi)有專門名字去稱呼它。與這種同步方式相對(duì)的,便是異步。即方法的開(kāi)始到結(jié)束可以由不同的線程調(diào)度執(zhí)行的編程方式,被成為異步編程。
異步編程技術(shù)目的,重點(diǎn)并非提高并發(fā)能力,而是提高伸縮性 (Scalability)。現(xiàn)在的 Web 服務(wù),應(yīng)付 QPS 幾百上千,甚至上萬(wàn)的場(chǎng)景并沒(méi)有太大問(wèn)題,但問(wèn)題是如何在并發(fā)請(qǐng)求量突增的場(chǎng)景中提供穩(wěn)定服務(wù)呢?如果一個(gè)應(yīng)用能穩(wěn)定提供 QPS 1000的服務(wù)。假如在某一個(gè)大促活動(dòng)中,這個(gè)應(yīng)用的 QPS 突然增加到10000怎么辦?或者 QPS 沒(méi)變,但這個(gè)應(yīng)用所依賴的服務(wù)發(fā)生故障,或網(wǎng)絡(luò)超時(shí)。當(dāng)這些情況發(fā)生時(shí),服務(wù)還能穩(wěn)定提供嗎?雖然熔斷、限流等技術(shù)能夠解決這種場(chǎng)景下服務(wù)的可用性問(wèn)題,但這畢竟是一種舍車保帥的做法。是否能在流量突增時(shí)仍保證服務(wù)質(zhì)量呢?答案是肯定的,那就是異步編程 + NIO。NIO 技術(shù)本身現(xiàn)在已經(jīng)很成熟了,關(guān)鍵是用一種什么樣的異步編程技術(shù)將 NIO 落地到系統(tǒng),尤其是業(yè)務(wù)快速迭代的前臺(tái)、中臺(tái)系統(tǒng)中。
這就是本文討論 Java 異步編程的原因。Java 應(yīng)用開(kāi)發(fā)領(lǐng)域究竟有哪些技術(shù)可以用來(lái)提升系統(tǒng)的伸縮性?本文將按照這些技術(shù)的演化歷程,介紹一下這些技術(shù)的意義和演化過(guò)程:
- Future
- Callback
- Servlet 3.0
- 反應(yīng)式編程
- Kotlin 協(xié)程
- Project Loom
一、Future
J.U.C 中的 Future 算是 Java 對(duì)異步編程的第一個(gè)解決方案。當(dāng)向線程池 submit 一個(gè)任務(wù)后,這個(gè)任務(wù)便被另一個(gè)線程執(zhí)行了:
- Future future = threadPool.submit(() -> { foobar(); return result;});Object result = future.get();
但這個(gè)解決方案有很多缺陷:
- 無(wú)法方便得知任務(wù)何時(shí)完成
- 無(wú)法方便獲得任務(wù)結(jié)果
- 在主線程獲得任務(wù)結(jié)果會(huì)導(dǎo)致主線程阻塞
二、Callback
為了解決使用 Future 所存在的問(wèn)題,人們提出了一個(gè)叫 Callback 的解決方案。比如 Google Guava 包中的 ListenableFuture 就是基于此實(shí)現(xiàn)的:
- ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() { public Explosion call() { return pushBigRedButton(); }});Futures.addCallback(explosion, new FutureCallback<Explosion>() { // we want this handler to run immediately after we push the big red button! public void onSuccess(Explosion explosion) { walkAwayFrom(explosion); } public void onFailure(Throwable thrown) { battleArchNemesis(); // escaped the explosion! }});
通過(guò)執(zhí)行 ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {}) 創(chuàng)建異步任務(wù)。通過(guò) Futures.addCallback(explosion, new FutureCallback<Explosion>() {} 添加處理結(jié)果的回調(diào)函數(shù)。這樣避免獲取并處理異步任務(wù)執(zhí)行結(jié)果阻塞調(diào)起線程的問(wèn)題。Callback 是將任務(wù)執(zhí)行結(jié)果作為接口的入?yún)ⅲ谌蝿?wù)完成時(shí)回調(diào) Callback 接口,執(zhí)行后續(xù)任務(wù),從而解決純 Future 方案無(wú)法方便獲得任務(wù)執(zhí)行結(jié)果的問(wèn)題。
但 Callback 產(chǎn)生了新的問(wèn)題,那就是代碼可讀性的問(wèn)題。因?yàn)槭褂?Callback 之后,代碼的字面形式和其所表達(dá)的業(yè)務(wù)含義不匹配,即業(yè)務(wù)的先后關(guān)系到了代碼層面變成了包含和被包含的關(guān)系。
因此,如果大量使用 Callback 機(jī)制,將使大量的應(yīng)該是先后的業(yè)務(wù)邏輯在代碼形式上表現(xiàn)為層層嵌套。這會(huì)導(dǎo)致代碼難以理解和維護(hù)。這便是所謂的 Callback Hell(回調(diào)地獄)問(wèn)題。
Callback Hell 問(wèn)題可以從兩個(gè)方向進(jìn)行一定的解決:一是事件驅(qū)動(dòng)機(jī)制、二是鏈?zhǔn)秸{(diào)用。前者被如 Vert.x 所使用,后者被 CompletableFuture、反應(yīng)式編程等技術(shù)采用。但這些優(yōu)化的效果有限,不能根本上解決 Callback 機(jī)制所帶來(lái)的代碼可維護(hù)性的下降。
Callback 與 NIO
Callback 真正體現(xiàn)價(jià)值,是它與 NIO 技術(shù)結(jié)合之后。原因也很簡(jiǎn)單:對(duì)于 CPU 密集型應(yīng)用,采用 Callback 風(fēng)格沒(méi)有意義;對(duì)于 IO 密集型應(yīng)用,如果是使用 BIO,Callback 同樣沒(méi)有意義,因?yàn)樽罱K會(huì)有一個(gè)線程是因?yàn)?IO 而阻塞。而只有使用 NIO 才能避免線程阻塞,也必須使用 Callback 風(fēng)格,才能使應(yīng)用得以被開(kāi)發(fā)出來(lái)。NIO 的廣泛應(yīng)用是在 Apache Mina、JBoss Netty 等技術(shù)出現(xiàn)之后。這些技術(shù)很大程度地簡(jiǎn)化了 NIO 技術(shù)的使用,但直接使用它們開(kāi)發(fā)業(yè)務(wù)系統(tǒng)還是很繁瑣。
下面看一個(gè)真實(shí)的例子。這個(gè)例子背后的完整應(yīng)用的功能是將微軟 Exchange 服務(wù)接口(Exchange Web Service)轉(zhuǎn)換為 Rest 風(fēng)格的接口,下面這段代碼是這個(gè)應(yīng)用的一部分。
- public class EwsCalendarHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(final ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { final HttpRequest origReq = (HttpRequest) msg; HttpRequest request = translateRequest(origReq); if (backendChannel == null) { connectBackendFuture = connectBackend(ctx, StaticConfiguration.EXCHANGE_PORT); sendMessageAfterConnected(ctx, request); } else if (backendChannel.isActive()) { setHttpRequestToBackendHandler(request); sendObjectAndFlush(ctx, request); } else { sendMessageAfterConnected(ctx, request); } } else if (msg instanceof HttpContent) { HttpContent content = (HttpContent) msg; if (backendChannel == null || !backendChannel.isActive()) { sendMessageAfterConnected(ctx, content); } else { sendObjectAndFlush(ctx, content); } } } private void sendMessageAfterConnected(final ChannelHandlerContext ctx, final HttpObject message) { if (connectBackendFuture == null) { LOGGER.warn("next hop connect future is null, drop the message and return: {}", message); return; } connectBackendFuture.addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { ChannelFuture f = sendObjectAndFlush(ctx, message); if (f != null) { f.addListener((future1) -> backendChannel.attr(FIND_ITEM_START_ATTR_KEY).set(System.currentTimeMillis()) ); } } }); }}
在方法 sendMessageAfterConnected 中,我們已經(jīng)能看到嵌套兩層的 Callback。而上面實(shí)例中的 EwsCalendarHandler 所實(shí)現(xiàn)的 ChannelInboundHandler 接口,本質(zhì)上也是一個(gè)回調(diào)接口。
其實(shí)上面的例子只有一級(jí)服務(wù)調(diào)用。在微服務(wù)流行的今天,多級(jí)服務(wù)調(diào)用很常見(jiàn),一個(gè)服務(wù)先調(diào) A,再用結(jié)果 A 調(diào) B,然后用結(jié)果 B 調(diào)用 C,等等。這樣的場(chǎng)景,如果直接用 Netty 開(kāi)發(fā),技術(shù)難度會(huì)比傳統(tǒng)方式增加很多。這其中的難度來(lái)自兩方面,一是 NIO 和 Netty 本身的技術(shù)難度,二是 Callback 風(fēng)格所導(dǎo)致的代碼理解和維護(hù)的困難。
因此,直接使用 Netty,通常局限在基礎(chǔ)架構(gòu)層面,在前臺(tái)和中臺(tái)業(yè)務(wù)系統(tǒng)中,應(yīng)用較少。
三、Servlet 3.0
上面講到,如果直接使用 Netty 開(kāi)發(fā)應(yīng)用,將不可避免地遇到 Netty 和 NIO 本身的技術(shù)挑戰(zhàn),以及 Callback Hell 問(wèn)題。對(duì)于前者,Servlet 3.0 提供了一個(gè)解決方案。
▼ 示例:Servlet 3.0 ▼
- @WebServlet(urlPatterns = "/demo", asyncSupported = true)public class AsyncDemoServlet extends HttpServlet { @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // Do Something AsyncContext ctx = req.startAsync(); startAsyncTask(ctx); }} private void startAsyncTask(AsyncContext ctx) { requestRpcService(result -> { try { PrintWriter out = ctx.getResponse().getWriter(); out.println(result); out.flush(); ctx.complete(); } catch (Exception e) { e.printStackTrace(); } });}
Servlet 3.0 的出現(xiàn),解決了在過(guò)去基于 Servlet 的 Web 應(yīng)用中,接受請(qǐng)求和返回響應(yīng)必須在同一個(gè)線程的問(wèn)題,實(shí)現(xiàn)了如下目標(biāo):
- 可以避免了 Web 容器的線程被阻塞掛起
- 使請(qǐng)求接收之后的任務(wù)處理可由專門線程完成
- 不同任務(wù)可以實(shí)現(xiàn)線程池隔離
- 結(jié)合 NIO 技術(shù)實(shí)現(xiàn)更高效的 Web 服務(wù)
除了直接使用 Servlet 3.0,也可以選擇 Spring MVC 的 Deferred Result。
▼ 示例:Spring MVC DeferredResult ▼
- @GetMapping("/async-deferredresult")public DeferredResult<ResponseEntity<?>> handleReqDefResult(Model model) { LOG.info("Received async-deferredresult request"); DeferredResult<ResponseEntity<?>> output = new DeferredResult<>(); ForkJoinPool.commonPool().submit(() -> { LOG.info("Processing in separate thread"); try { Thread.sleep(6000); } catch (InterruptedException e) { } output.setResult(ResponseEntity.ok("ok")); }); LOG.info("servlet thread freed"); return output;}
Servlet 3.0 的技術(shù)局限
Servlet 3.0 并不是用來(lái)解決前面提到的 Callback Hell 問(wèn)題的,它只是降低了異步 Web 編程的技術(shù)門檻。對(duì)于 Callback Hell 問(wèn)題,使用 Servlet 3.0 或類似技術(shù)時(shí)同樣會(huì)遇到。解決 Callback Hell 還需另尋他法。
四、反應(yīng)式編程
現(xiàn)在擋在異步編程最大的障礙就是 Callback Hell,因?yàn)?Callback Hell 對(duì)代碼可讀性有很大殺傷力。而本節(jié)介紹的反應(yīng)式編程技術(shù),除了響應(yīng)性、伸縮性、容錯(cuò)性以外,從開(kāi)發(fā)人員的角度來(lái)講,就是代碼可讀性要比 Callback 提升了許多。
▼ 圖:反應(yīng)式編程的特性 ▼

▼ 反應(yīng)式編程簡(jiǎn)單示例 ▼
- userService.getFavorites(userId) .flatMap(favoriteService::getDetails) .switchIfEmpty(suggestionService.getSuggestions()) .take(5) .publishOn(UiUtils.uiThreadScheduler()) .subscribe(uiList::show, UiUtils::errorPopup);
可讀性的提高原因在于反應(yīng)式編程可讓開(kāi)發(fā)人員將實(shí)現(xiàn)業(yè)務(wù)的各種方法使用鏈?zhǔn)剿阕哟?lián)起來(lái),而串聯(lián)起來(lái)的各種方法的先后關(guān)系與執(zhí)行順序大體一致。
這其實(shí)是采用了函數(shù)式編程的設(shè)計(jì),通過(guò)函數(shù)式編程解決了之前 Callback 設(shè)計(jì)存在的代碼可讀性問(wèn)題。
雖然相對(duì)于 Callback,代碼可讀性是反應(yīng)式編程的優(yōu)點(diǎn),但這種優(yōu)點(diǎn)是相對(duì)的,相對(duì)于傳統(tǒng)代碼,可讀性就成了反應(yīng)式編程的缺點(diǎn)。上面的例子代碼看上去還容易理解,但換成下面的例子,大家就又能重新看到 Callback Hell 的影子了:
▼ 示例:查詢最近郵件數(shù)(反應(yīng)式編程版) ▼
- @GetMapping("/reactive/{personId}")fun getMessagesFor(@PathVariable personId: String): Mono<String> { return peopleRepository.findById(personId) .switchIfEmpty(Mono.error(NoSuchElementException())) .flatMap { person -> auditRepository.findByEmail(person.email) .flatMap { lastLogin -> messageRepository.countByMessageDateGreaterThanAndEmail(lastLogin.eventDate, person.email) .map { numberOfMessages -> "Hello ${person.name}, you have $numberOfMessages messages since ${lastLogin.eventDate}" } } }}
因此,反應(yīng)式編程只看代碼形式,可以被視為 Callback 2.0。解決了之前的一些問(wèn)題,但并不徹底。
目前,在 Java 領(lǐng)域?qū)崿F(xiàn)了反應(yīng)式編程的技術(shù)有 Spring 的 Project Reactor、Netflix RxJava 1/2 等。前者的 3.0 版本作為 Spring 5 的基礎(chǔ),在17年底發(fā)布,推動(dòng)了后端領(lǐng)域反應(yīng)式編程的發(fā)展。后者出現(xiàn)時(shí)間更早,在前端開(kāi)發(fā)領(lǐng)域應(yīng)用的比后端更要廣泛一些。
除了開(kāi)源框架,JDK 也提供了對(duì)反應(yīng)式編程解決方案:JDK 8 的 CompletableFuture 不算是反應(yīng)式編程,但是它在形式上帶有一些反應(yīng)式編程的函數(shù)式代碼風(fēng)格。JDK 9 Flow 實(shí)現(xiàn)了 Reactive Streams 規(guī)范,但是實(shí)施反應(yīng)式編程需要完整的解決方案,單靠 Flow 是不夠的,還是需要 Project Reactor 這樣的完整解決方案。但 JDK 層面的技術(shù)能提供統(tǒng)一的技術(shù)抽象和實(shí)現(xiàn),在統(tǒng)一技術(shù)方面還是有積極意義的。
反應(yīng)式編程的應(yīng)用范圍
正如前面所說(shuō),反應(yīng)式編程仍然存在代碼可讀性的問(wèn)題,這個(gè)問(wèn)題在加上反應(yīng)式編程本身的技術(shù)門檻,使得用反應(yīng)式編程技術(shù)在業(yè)務(wù)系統(tǒng)開(kāi)發(fā)領(lǐng)域一直沒(méi)有流行普及。但是對(duì)于核心系統(tǒng)、底層系統(tǒng),反應(yīng)式編程技術(shù)所帶來(lái)的伸縮性、容錯(cuò)性的提升同其增加的開(kāi)發(fā)成本相比通常是可以接受。因此核心系統(tǒng)、底層系統(tǒng)是適合采用反應(yīng)式編程技術(shù)的。
五、Kotlin 協(xié)程
前面介紹的各種技術(shù),都有明顯的缺陷:Future 不是真異步;Callback 可讀性差;Servlet 3.0 等技術(shù)沒(méi)能解決 Callback 的缺陷;反應(yīng)式編程還是難以編寫復(fù)雜業(yè)務(wù)。到了18年,一種新的 JVM 編程語(yǔ)言開(kāi)始流行:Kotlin。Kotlin 首先流行在 Android 開(kāi)發(fā)領(lǐng)域,因?yàn)樗玫搅?Google 的首肯和支持。但對(duì)于后端開(kāi)發(fā)領(lǐng)域,因?yàn)橐豁?xiàng)特性,使得 Kotlin 也非常值得注意。那就是 Kotlin Coroutine(后文稱 Kotlin 協(xié)程)。對(duì)于這項(xiàng)技術(shù),我已經(jīng)寫過(guò)三篇文章,分別介紹入門、原理和與 Spring Project Reactor 的整合方式。感興趣的同學(xué)可以去我的簡(jiǎn)書(shū)和微信公眾號(hào)上去看這些文章(搜索“編走編想”)。
協(xié)程技術(shù)不是什么新技術(shù),它在很多語(yǔ)言中都有實(shí)現(xiàn),比如大家所熟悉的 Python、Lua、Go 都是支持協(xié)程的。在不同語(yǔ)言中,協(xié)程的實(shí)現(xiàn)方法各有不同。因?yàn)?Kotlin 的運(yùn)行依賴于 JVM,不能對(duì) JVM 進(jìn)行修改,因此,Kotlin 不能在底層支持協(xié)程。同時(shí),Kotlin 是一門編程語(yǔ)言,需要在語(yǔ)言層面支持協(xié)程,而不是像框架那樣在語(yǔ)言層面之上支持。因此,Kotlin 對(duì)協(xié)程支持最核心的部分是在編譯器中。因?yàn)閷?duì)這部分原理的解釋在之前文章中都有涉及,因此不在這里重復(fù)。
使用 Kotlin 協(xié)程之后最大的好處是異步代碼的可讀性大大提高。如果上一個(gè)示例用 Kotlin 協(xié)程實(shí)現(xiàn),那就是下面的樣子:
▼ 示例:查詢最近郵件數(shù)(Kotlin 協(xié)程版) ▼
- @GetMapping("/coroutine/{personId}")fun getNumberOfMessages(@PathVariable personId: String) = mono(Unconfined) { val person = peopleRepository.findById(personId).awaitFirstOrDefault(null) ?: throw NoSuchElementException("No person can be found by $personId") val lastLoginDate = auditRepository.findByEmail(person.email).awaitSingle().eventDate val numberOfMessages = messageRepository.countByMessageDateGreaterThanAndEmail(lastLoginDate, person.email).awaitSingle() "Hello ${person.name}, you have $numberOfMessages messages since $lastLoginDate"}
目前在 Spring 應(yīng)用中使用 Kotlin 協(xié)程還有些小繁瑣,但在 Spring Boot 2.2 中,可以直接在 Spring WebFlux 方法上使用 suspend 關(guān)鍵字。
Kotlin 協(xié)程最大的意義就是可以用看似指令式編程方式(Imperative Programming
,即傳統(tǒng)編程方式)去寫異步編程代碼。并發(fā)和代碼可讀性似乎兩全其美了。
Kotlin 協(xié)程的局限性
但事情不是那么完美。Kotlin 協(xié)程依賴于各種基于 Callback 的技術(shù)。像上面的例子,之所以可以用 Kotlin 協(xié)程,是因?yàn)樯弦粋€(gè)版本使用了反應(yīng)式編程技術(shù)。所以,只有當(dāng)一段代碼使用了 ListenableFuture、CompletableFuture、Project Reactor、RxJava 等技術(shù)時(shí),才能用 Kotlin 協(xié)程進(jìn)行改造優(yōu)化。那對(duì)于其它的會(huì)阻塞線程的技術(shù),如 Object.wait、Thread.sleep、Lock、BIO 等,Kotlin 協(xié)程就無(wú)能為力了。
另外一個(gè)局限性源于 Kotlin 本身。雖然 Kotlin 兼容 Java,但這種兼容并非完美。因此,對(duì)于組件,尤其是基礎(chǔ)組件的開(kāi)發(fā),并不推薦使用 Kotlin,而是更推薦使用 Java。這也導(dǎo)致 Kotlin 協(xié)程的使用范圍被進(jìn)一步地限制。
六、Project Loom
前面講到,雖然 Kotlin 協(xié)程看上去很好,但在使用上還是有著種種限制。那有沒(méi)有更好的選擇呢?答案是 Project Loom (https://openjdk.java.net/projects/loom/)。這個(gè)項(xiàng)目在18年底的時(shí)候已經(jīng)達(dá)到可初步演示的原型階段。不同于之前的方案,Project Loom 是從 JVM 層面對(duì)多線程技術(shù)進(jìn)行徹底的改變。
Project Loom 設(shè)計(jì)思想與之前的一個(gè)開(kāi)源 Java 協(xié)程技術(shù)非常相似。這個(gè)技術(shù)就是 Quasar Fiber https://docs.paralleluniverse.co/quasar/ 。而現(xiàn)在 Project Loom 的主要設(shè)計(jì)開(kāi)發(fā)人員 Ron Pressler 就是來(lái)自 Quasar Fiber。
這里建議大家讀一下 Project Loom 的這篇文檔:http://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html。這篇文檔介紹了發(fā)起 Project Loom 的原因,以及 Java 線程基礎(chǔ)的很多底層設(shè)計(jì)。
其實(shí)發(fā)起 Project Loom 的原因也很簡(jiǎn)單:長(zhǎng)期以來(lái),Java 的線程是與操作系統(tǒng)的線程一一對(duì)應(yīng)的,這限制了 Java 平臺(tái)并發(fā)能力的提升。各種框架或其它 JVM 編程語(yǔ)言的解決方案,都在使用場(chǎng)景上有限制。例如 Kotlin 協(xié)程必須基于各種 Callback 技術(shù),而 Callback 技術(shù)有存在編寫、調(diào)試?yán)щy的問(wèn)題。為了使 Java 并發(fā)能力在更大范圍上得到提升,從底層進(jìn)行改進(jìn)便是必然。
下面這幅圖很好地展示了目前 Java 并發(fā)編程方面的困境,簡(jiǎn)單的代碼并發(fā)、伸縮能力差;并發(fā)、伸縮能力強(qiáng)的代碼復(fù)雜,難以與現(xiàn)有代碼整合。

為了讓簡(jiǎn)單和高并發(fā)這兩個(gè)目標(biāo)兼得,我們需要 Project Loom 這個(gè)項(xiàng)目。
使用方法
在引入 Project Loom 之后,JDK 將引入一個(gè)新類:java.lang.Fiber。此類與 java.lang.Thread 一起,都成為了 java.lang.Strand 的子類。即線程變成了一個(gè)虛擬的概念,有兩種實(shí)現(xiàn)方法:Fiber 所表示的輕量線程和 Thread 所表示的傳統(tǒng)的重量級(jí)線程。
對(duì)于應(yīng)用開(kāi)發(fā)人員,使用 Project Loom 很簡(jiǎn)單:
- Fiber f = Fiber.schedule(() -> { println("Hello 1"); lock.lock(); // 等待鎖不會(huì)掛起線程 try { println("Hello 2"); } finally { lock.unlock(); } println("Hello 3");})
只需執(zhí)行 Fiber.schedule(Runnable task) 就能在 Fiber 中執(zhí)行任務(wù)。**最重要的是,上面例子中的 lock.lock() 操作將不再掛起底層線程。除了 Lock 不再掛起線程以外,像 Socket BIO 操作也不再掛起線程。** 但 synchronized,以及 Native 方法中線程掛起操作無(wú)法避免。
- synchronized (monitor) { // 在 Fiber 中調(diào)用這條語(yǔ)句還是會(huì)掛起線程。 socket.getInputStream().read();}
如上所示,F(xiàn)iber 的使用非常簡(jiǎn)單。因此,讓現(xiàn)有系統(tǒng)使用 Project Loom 很容易。像 Tomcat、Jetty 這樣的 Web 容器,只需將處理請(qǐng)求操作從使用 ThreadPoolExecutor execute 或 submit 改為使用 Fiber schedule 即可。這個(gè)視頻 https://www.youtube.com/watch?v=vbGbXUjlRyQ&t=1240s 中的 Demo 展示了 Jetty 使用 Project Loom 改造之后并發(fā)吞吐能力的大幅提升。
實(shí)現(xiàn)原理
接下來(lái)簡(jiǎn)單介紹一下 Project Loom 的實(shí)現(xiàn)原理。Project Loom 的使用主要基于 Fiber,而實(shí)現(xiàn)則主要基于 Continuation。Contiuation 表示一個(gè)可暫停和恢復(fù)的計(jì)算單元。在 Project Loom 中,Continuationn 使用 java.lang.Continuation 類實(shí)現(xiàn)。這個(gè)類主要供類庫(kù)實(shí)現(xiàn)使用,而不是直接被應(yīng)用開(kāi)發(fā)人員使用。Continuation 主要內(nèi)容如下所示:
- package java.lang;public class Continuation implements Runnable { public Continuation(ContinuationScope scope, Runnable target) public final void run() public static void yield(ContinuationScope scope) public boolean isDone()}
Continuation 實(shí)現(xiàn)了 Runnable 接口,構(gòu)造時(shí)除了需要提供一個(gè) Runnable 類型的參數(shù)以外,還需要提供一個(gè) java.lang.ContinuationScope 的參數(shù)。ContinuationScope 顧名思義表示 Continuation 的范圍。Continuation 可以被想象成是一個(gè)方法執(zhí)行過(guò)程,方法可以調(diào)用其它方法。同時(shí),方法執(zhí)行也有一定的影響范圍,如 try...catch 就規(guī)定了相應(yīng)的范圍。ContinuationScope 就起到了起到了相應(yīng)的作用。
Continuation 有兩個(gè)最重要的方法:run 和 yield。run 方法首次被調(diào)用時(shí),就會(huì)執(zhí)行 Runnable target 的 run 方法。但是,在調(diào)用了 yield 方法后,再次調(diào)用 run 方法,Continuation 就不會(huì)從頭執(zhí)行,而是從 yield 的位置開(kāi)始執(zhí)行。
為了更形象的理解,下面看一個(gè)例子:
- Continuation con = new Continuation(SCOPE, () -> { println("A"); Continuation.yield(SCOPE); println("B"); Continuation.yield(SCOPE); println("C");});con.run();con.run();con.run();
輸出結(jié)果:
- ABC
上面的例子非常簡(jiǎn)單:創(chuàng)建一個(gè) Continuation,其 Runnable target 打印 A、B、C,并在其中 yield 兩次。創(chuàng)建之后調(diào)用三次 run() 方法。如果這樣執(zhí)行一個(gè)普通的 Runnable,那應(yīng)該打印三次 A、B、C,一共打印九次。而 Continuation 在 yield 之后執(zhí)行 run,會(huì)從 yield 的位置往后執(zhí)行,而不是從頭開(kāi)始。
Continuation yield 類似 Thread 的 yield,但前者需要顯式調(diào)用 run 方法恢復(fù)執(zhí)行。
在 Project Loom 之后,LockSupport 的 park 操作將變?yōu)椋?/p>
- public class LockSupport { var strand = Strands.currentStrand(); if (strand instanceof Fiber) { Continuation.yield(FIBER_SCOPE); } else { Unsafe.park(false, 0L); }}
七、展望
Java 作為使用率最高的編程軟件,在包括后端開(kāi)發(fā)、手機(jī)應(yīng)用開(kāi)發(fā)、大數(shù)據(jù)等眾多領(lǐng)域均有廣泛應(yīng)用。但畢竟是一門誕生20多年的編程語(yǔ)言,存在一些現(xiàn)在看來(lái)設(shè)計(jì)上的不足和受到后來(lái)者的挑戰(zhàn)都是正常。但必須說(shuō)明,我們口中的 Java 并非一門單純的編程語(yǔ)言。而應(yīng)該被視為 Java 語(yǔ)言 + JVM + Java 類庫(kù)三部分組成。這三部分中,毫無(wú)疑問(wèn),JVM 是基礎(chǔ)。但 JVM 設(shè)計(jì)之初就并非和 Java 語(yǔ)言緊密綁定,緊密綁定的只是字節(jié)碼。由任何編程語(yǔ)言編譯得到的合法字節(jié)碼都能運(yùn)行在 JVM 之上。這使得 Java 語(yǔ)言層面設(shè)計(jì)的不足可有其它編程語(yǔ)言解決,于是出現(xiàn)了 Groovy、Scala、Kotlin、Clojure 等眾多 JVM 語(yǔ)言。這些語(yǔ)言很大程度上彌補(bǔ)了 Java 的不足。
但像多線程這樣的技術(shù),由于和底層虛機(jī)和操作系統(tǒng)有千絲萬(wàn)縷的聯(lián)系,想要徹底改進(jìn),繞不開(kāi)底層優(yōu)化。這就是 Project Loom 出現(xiàn)的原因。相信 Project Loom 技術(shù)會(huì)將 Java 的并發(fā)能力提升至和 Golang 一樣的水平,而付出的成本只是對(duì)現(xiàn)有項(xiàng)目的少量改動(dòng)。
Azul 的 Deputy CTO Simon Ritter 曾透露 Project Loom 很可能在 Java 13 時(shí)發(fā)布。究竟能不能趕上 Java 13,這個(gè)不可知,好在 Java 13 的特性還未完全確定,說(shuō)不定可以 Project Loom 可以趕上末班車。
就算 Project Loom 沒(méi)能和 Java 13 一起發(fā)布。但目前反應(yīng)式編程的趨勢(shì)也非常明顯。隨著新版本的 Spring 和 Kotlin 的發(fā)布,反應(yīng)式編程的使用、調(diào)試變得越來(lái)越簡(jiǎn)單。Dubbo 也明確表示在 3.0 中將會(huì)支持 Project Reactor。R2DBC 在不久的未來(lái)也會(huì)支持 MySQL。因此,Java 異步編程將快速發(fā)展,在易用性方面迅速趕上甚至超過(guò) Go。
另一方面,開(kāi)發(fā)人員也不要將自己局限在某種特定技術(shù)上,對(duì)各種技術(shù)都保持開(kāi)放的態(tài)度是開(kāi)發(fā)人員技能不斷提高的前提。只會(huì)簡(jiǎn)單說(shuō)某某語(yǔ)言、某某技術(shù)比其它技術(shù)更好的技術(shù)人員永遠(yuǎn)不會(huì)成為出色的技術(shù)人員。