自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java 異步編程:從 Future 到 Loom

新聞 前端
眾所周知,Java 開(kāi)始方法執(zhí)行到結(jié)束,都是由同一個(gè)線程完成的。這種方式雖易于開(kāi)發(fā)調(diào)試,但容易因?yàn)殒i、IO 等原因?qū)е戮€程掛起,產(chǎn)生線程上下文切換。

 眾所周知,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í)行了:

  1. Future future = threadPool.submit(() -> {  foobar();  return result;});Object result = future.get(); 

但這個(gè)解決方案有很多缺陷:

  1. 無(wú)法方便得知任務(wù)何時(shí)完成
  2. 無(wú)法方便獲得任務(wù)結(jié)果
  3. 在主線程獲得任務(wù)結(jié)果會(huì)導(dǎo)致主線程阻塞

二、Callback

為了解決使用 Future 所存在的問(wèn)題,人們提出了一個(gè)叫 Callback 的解決方案。比如 Google Guava 包中的 ListenableFuture 就是基于此實(shí)現(xiàn)的:

  1. 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)用的一部分。

  1. 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 ▼

  1. @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):

  1. 可以避免了 Web 容器的線程被阻塞掛起
  2. 使請(qǐng)求接收之后的任務(wù)處理可由專門線程完成
  3. 不同任務(wù)可以實(shí)現(xiàn)線程池隔離
  4. 結(jié)合 NIO 技術(shù)實(shí)現(xiàn)更高效的 Web 服務(wù)

除了直接使用 Servlet 3.0,也可以選擇 Spring MVC 的 Deferred Result。

▼ 示例:Spring MVC DeferredResult ▼

  1. @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)式編程的特性 ▼

Java 異步編程:從 Future 到 Loom

▼ 反應(yīng)式編程簡(jiǎn)單示例 ▼

  1. 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)式編程版) ▼

  1. @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é)程版) ▼

  1. @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)有代碼整合。

Java 異步編程:從 Future 到 Loom

為了讓簡(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)單:

  1. 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ú)法避免。

  1. 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)容如下所示:

  1. 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è)例子:

  1. Continuation con = new Continuation(SCOPE, () -> {  println("A");  Continuation.yield(SCOPE);  println("B");  Continuation.yield(SCOPE);  println("C");});con.run();con.run();con.run(); 

輸出結(jié)果:

  1. 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>

  1. 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ù)人員。

 

責(zé)任編輯:張燕妮 來(lái)源: 今日頭條
相關(guān)推薦

2025-02-06 16:51:30

2020-08-10 07:58:18

異步編程調(diào)用

2023-11-24 16:13:05

C++編程

2013-04-01 15:38:54

異步編程異步編程模型

2020-09-24 08:45:10

React架構(gòu)源碼

2025-02-24 00:10:00

2021-12-12 18:15:06

Python并發(fā)編程

2022-06-16 13:08:30

Combine響應(yīng)式編程訂閱

2017-03-13 09:19:38

CAP編程語(yǔ)言

2022-03-31 07:52:01

Java多線程并發(fā)

2021-03-22 08:45:30

異步編程Java

2021-08-02 11:13:28

人工智能機(jī)器學(xué)習(xí)技術(shù)

2020-10-15 13:29:57

javascript

2013-04-01 15:25:41

異步編程異步EMP

2025-04-21 04:00:00

2011-02-22 09:09:21

.NETAsync CTP異步

2011-02-22 08:49:16

.NET同步異步

2021-02-21 14:35:29

Java 8異步編程

2020-12-07 09:40:19

Future&Futu編程Java

2024-11-25 18:37:09

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)