1. 首页
  2. 综合百科
  3. 流是什么意思(Java)

流是什么意思(Java)

简介:关于流是什么意思(Java)的相关疑问,相信很多朋友对此并不是非常清楚,为了帮助大家了解相关知识要点,小编为大家整理出如下讲解内容,希望下面的内容对大家有帮助!
如果有更好的建议或者想看更多关于综合百科技术大全及相关资讯,可以多多关注茶馆百科网。

众所周知,Java start方法是由同一个线程执行到底的。这种方法虽然开发调试简单,但是容易因为锁、IO等原因造成线程挂起和线程上下文切换。随着应用并发性需求的增加,频繁的线程上下文切换的成本变得难以忽视。同时,线程是相对宝贵的资源,不可能无限制的增加线程。优秀的技术人员应该能够让应用使用更少的线程资源达到更高的并发。这就是我们今天要讨论的话题:——Java异步编程技术。

异步编程没有明确的定义。一般来说,我们认为从一个方法开始到结束都必须在同一个线程中调度执行的编程模式可以认为是同步编程模式。但因为这种方式是我们习以为常的,所以没有专门的名称来称呼它。与这种同步方法相比,它是异步的。即一个方法的开始和结束可以被不同的线程调度和执行的编程模式称为异步编程。

异步编程技术目的,重点并非提高并发能力,而是提高伸缩性(Scalability).现在Web服务应对几百上千甚至上万的QPS场景没有太大问题,但问题是在并发请求骤增的场景下,如何提供稳定的服务。如果一个应用能够稳定地提供QPS1000服务。如果这个应用的QPS在一次大促销活动中突然增加到10000怎么办?或者QPS没有改变,但是该应用程序所依赖的服务已经失败,或者网络已经超时。当这些情况发生时,服务能否稳定提供?虽然熔断、限流等技术可以解决这种场景下的服务可用性问题,但这是一种舍车保帅的做法。是否能在流量突增时仍保证服务质量呢?答案是肯定的,那就是异步编程+NIO。NIO技术本身现在已经很成熟了,关键是用一种什么样的异步编程技术将NIO落地到系统,尤其是业务快速迭代的前台、中台系统中。

这就是本文讨论Java异步编程的原因。Java应用开发领域究竟有哪些技术可以用来提升系统的伸缩性?本文将按照这些技术的演化历程,介绍一下这些技术的意义和演化过程:

FutureCallbackServlet3.0反应式编程kot Lin Collaborative project loom

一、Future

J . u . c是Java中第一个异步编程的解决方案。当任务提交到线程池时,它由另一个线程执行:

futurefuture=thread pool . submit(()-{ foobar());返回结果;});object result=future . get();

但是这种解决方案有许多缺点:

不方便知道任务何时完成,不方便获取任务结果,会导致主线程阻塞

二、Callback

。为了解决未来使用中存在的问题,人们提出了一种叫做回调的解决方案。比如GoogleGuava包中的ListenableFuture就是基于此实现的:

ListeningExecutorServiceservice=more executors . listeningdecorator(executors . newfixedthreadpool(10));ListenableFutureExplosionexplosion=service . submit(newCallableExplosion(){ public explosion call(){ return bigredbutton();}});Futures.addCallback(explosion,newFutureCallbackExplosion(){//wewanthishhandlertoroutrunimmediatelyafterwepushthebigredbutton!publicfoidonsuccess(explosion explosion){ walk away from(explosion);} publicfoidonfailure(Throwablethrown){ battleArchNemesis();//escapedtheexplosion!}});

通过执行listablefutureexplosiondisclose=service来创建异步任务。提交(newCallableDisclosure () {})。添加一个回调函数,通过期货来处理结果。addcallback (explosion,newfuturecallbackexplosion () {}。这样就避免了获取和处理异步任务的执行结果阻塞启动线程的问题。回调以任务执行结果作为接口的参数,在任务完成时回调回调接口执行后续任务,解决了纯未来方案不容易获得任务执行结果的问题。

但是回调产生了一个新的问题,就是代码的可读性。因为使用回调后,代码的字面形式与其表达的业务含义不匹配,即业务的顺序从代码层面变成了包含与被包含的关系。

因此,如果大量使用Callback机制,将使大量的应该是先后的业务逻辑在代码形式上表现为层层嵌套。这会导致代码难以理解和维护。这便是所谓的CallbackHell(回调地狱)问题。



CallbackHell问题可以从两个方向进行一定的解决:一是事件驱动机制、二是链式调用。前者被如Vert.x所使用,后者被CompletableFuture、反应式编程等技术采用。但这些优化的效果有限,不能根本上解决Callback机制所带来的代码可维护性的下降。

Callback与NIO

Callback真正体现价值,是它与NIO技术结合之后。原因也很简单:对于CPU密集型应用,采用Callback风格没有意义;对于IO密集型应用,如果是使用BIO,Callback同样没有意义,因为最终会有一个线程是因为IO而阻塞。而只有使用NIO才能避免线程阻塞,也必须使用Callback风格,才能使应用得以被开发出来。NIO的广泛应用是在ApacheMina、JBossNetty等技术出现之后。这些技术很大程度地简化了NIO技术的使用,但直接使用它们开发业务系统还是很繁琐。



下面看一个真实的例子。这个例子背后的完整应用的功能是将微软Exchange服务接口(ExchangeWebService)转换为Rest风格的接口,下面这段代码是这个应用的一部分。



publicclassEwsCalendarHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(finalChannelHandlerContextctx,Objectmsg){if(msginstanceofHttpRequest){finalHttpRequestorigReq=(HttpRequest)msg;HttpRequestrequest=translateRequest(origReq);if(backendChannel==null){connectBackendFuture=connectBackend(ctx,StaticConfiguration.EXCHANGE_PORT);sendMessageAfterConnected(ctx,request);}elseif(backendChannel.isActive()){setHttpRequestToBackendHandler(request);sendObjectAndFlush(ctx,request);}else{sendMessageAfterConnected(ctx,request);}}elseif(msginstanceofHttpContent){HttpContentcontent=(HttpContent)msg;if(backendChannel==null||!backendChannel.isActive()){sendMessageAfterConnected(ctx,content);}else{sendObjectAndFlush(ctx,content);}}}privatevoidsendMessageAfterConnected(finalChannelHandlerContextctx,finalHttpObjectmessage){if(connectBackendFuture==null){LOGGER.warn("nexthopconnectfutureisnull,dropthemessageandreturn:{}",message);return;}connectBackendFuture.addListener((ChannelFutureListener)future->{if(future.isSuccess()){ChannelFuturef=sendObjectAndFlush(ctx,message);if(f!=null){f.addListener((future1)->backendChannel.attr(FIND_ITEM_START_ATTR_KEY).set(System.currentTimeMillis()));}}});}}



在方法sendMessageAfterConnected中,我们已经能看到嵌套两层的Callback。而上面实例中的EwsCalendarHandler所实现的ChannelInboundHandler接口,本质上也是一个回调接口。



其实上面的例子只有一级服务调用。在微服务流行的今天,多级服务调用很常见,一个服务先调A,再用结果A调B,然后用结果B调用C,等等。这样的场景,如果直接用Netty开发,技术难度会比传统方式增加很多。这其中的难度来自两方面,一是NIO和Netty本身的技术难度,二是Callback风格所导致的代码理解和维护的困难。



因此,直接使用Netty,通常局限在基础架构层面,在前台和中台业务系统中,应用较少。

三、Servlet3.0



上面讲到,如果直接使用Netty开发应用,将不可避免地遇到Netty和NIO本身的技术挑战,以及CallbackHell问题。对于前者,Servlet3.0提供了一个解决方案。



▼示例:Servlet3.0▼

@WebServlet(urlPatterns="/demo",asyncSupported=true)publicclassAsyncDemoServletextendsHttpServlet{@OverridepublicvoiddoGet(HttpServletRequestreq,HttpServletResponseresp)throwsIOException,ServletException{//DoSomethingAsyncContextctx=req.startAsync();startAsyncTask(ctx);}}privatevoidstartAsyncTask(AsyncContextctx){requestRpcService(result->{try{PrintWriterout=ctx.getResponse().getWriter();out.println(result);out.flush();ctx.complete();}catch(Exceptione){e.printStackTrace();}});}



Servlet3.0的出现,解决了在过去基于Servlet的Web应用中,接受请求和返回响应必须在同一个线程的问题,实现了如下目标:



可以避免了Web容器的线程被阻塞挂起使请求接收之后的任务处理可由专门线程完成不同任务可以实现线程池隔离结合NIO技术实现更高效的Web服务



除了直接使用Servlet3.0,也可以选择SpringMVC的DeferredResult。



▼示例:SpringMVCDeferredResult▼

@GetMapping("/async-deferredresult")publicDeferredResult<ResponseEntity<?>>handleReqDefResult(Modelmodel){LOG.info("Receivedasync-deferredresultrequest");DeferredResult<ResponseEntity<?>>output=newDeferredResult<>();ForkJoinPool.commonPool().submit(()->{LOG.info("Processinginseparatethread");try{Thread.sleep(6000);}catch(InterruptedExceptione){}output.setResult(ResponseEntity.ok("ok"));});LOG.info("servletthreadfreed");returnoutput;}



Servlet3.0的技术局限

Servlet3.0并不是用来解决前面提到的CallbackHell问题的,它只是降低了异步Web编程的技术门槛。对于CallbackHell问题,使用Servlet3.0或类似技术时同样会遇到。解决CallbackHell还需另寻他法。

四、反应式编程

现在挡在异步编程最大的障碍就是CallbackHell,因为CallbackHell对代码可读性有很大杀伤力。而本节介绍的反应式编程技术,除了响应性、伸缩性、容错性以外,从开发人员的角度来讲,就是代码可读性要比Callback提升了许多。



▼图:反应式编程的特性▼





▼反应式编程简单示例▼

userService.getFavorites(userId).flatMap(favoriteService::getDetails).switchIfEmpty(suggestionService.getSuggestions()).take(5).publishOn(UiUtils.uiThreadScheduler()).subscribe(uiList::show,UiUtils::errorPopup);



可读性的提高原因在于反应式编程可让开发人员将实现业务的各种方法使用链式算子串联起来,而串联起来的各种方法的先后关系与执行顺序大体一致。



这其实是采用了函数式编程的设计,通过函数式编程解决了之前Callback设计存在的代码可读性问题。



虽然相对于Callback,代码可读性是反应式编程的优点,但这种优点是相对的,相对于传统代码,可读性就成了反应式编程的缺点。上面的例子代码看上去还容易理解,但换成下面的例子,大家就又能重新看到CallbackHell的影子了:



▼示例:查询最近邮件数(反应式编程版)▼



@GetMapping("/reactive/{personId}")fungetMessagesFor(@PathVariablepersonId:String):Mono<String>{returnpeopleRepository.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},youhave$numberOfMessagesmessagessince${lastLogin.eventDate}"}}}}



因此,反应式编程只看代码形式,可以被视为Callback2.0。解决了之前的一些问题,但并不彻底。



目前,在Java领域实现了反应式编程的技术有Spring的ProjectReactor、NetflixRxJava1/2等。前者的3.0版本作为Spring5的基础,在17年底发布,推动了后端领域反应式编程的发展。后者出现时间更早,在前端开发领域应用的比后端更要广泛一些。



除了开源框架,JDK也提供了对反应式编程解决方案:JDK8的CompletableFuture不算是反应式编程,但是它在形式上带有一些反应式编程的函数式代码风格。JDK9Flow实现了ReactiveStreams规范,但是实施反应式编程需要完整的解决方案,单靠Flow是不够的,还是需要ProjectReactor这样的完整解决方案。但JDK层面的技术能提供统一的技术抽象和实现,在统一技术方面还是有积极意义的。



反应式编程的应用范围

正如前面所说,反应式编程仍然存在代码可读性的问题,这个问题在加上反应式编程本身的技术门槛,使得用反应式编程技术在业务系统开发领域一直没有流行普及。但是对于核心系统、底层系统,反应式编程技术所带来的伸缩性、容错性的提升同其增加的开发成本相比通常是可以接受。因此核心系统、底层系统是适合采用反应式编程技术的。



五、Kotlin协程

前面介绍的各种技术,都有明显的缺陷:Future不是真异步;Callback可读性差;Servlet3.0等技术没能解决Callback的缺陷;反应式编程还是难以编写复杂业务。到了18年,一种新的JVM编程语言开始流行:Kotlin。Kotlin首先流行在Android开发领域,因为它得到了Google的首肯和支持。但对于后端开发领域,因为一项特性,使得Kotlin也非常值得注意。那就是KotlinCoroutine(后文称Kotlin协程)。对于这项技术,我已经写过三篇文章,分别介绍入门、原理和与SpringProjectReactor的整合方式。感兴趣的同学可以去我的简书和微信公众号上去看这些文章(搜索“编走编想”)。



协程技术不是什么新技术,它在很多语言中都有实现,比如大家所熟悉的Python、Lua、Go都是支持协程的。在不同语言中,协程的实现方法各有不同。因为Kotlin的运行依赖于JVM,不能对JVM进行修改,因此,Kotlin不能在底层支持协程。同时,Kotlin是一门编程语言,需要在语言层面支持协程,而不是像框架那样在语言层面之上支持。因此,Kotlin对协程支持最核心的部分是在编译器中。因为对这部分原理的解释在之前文章中都有涉及,因此不在这里重复。



使用Kotlin协程之后最大的好处是异步代码的可读性大大提高。如果上一个示例用Kotlin协程实现,那就是下面的样子:



▼示例:查询最近邮件数(Kotlin协程版)▼

@GetMapping("/coroutine/{personId}")fungetNumberOfMessages(@PathVariablepersonId:String)=mono(Unconfined){valperson=peopleRepository.findById(personId).awaitFirstOrDefault(null)?:throwNoSuchElementException("Nopersoncanbefoundby$personId")vallastLoginDate=auditRepository.findByEmail(person.email).awaitSingle().eventDatevalnumberOfMessages=messageRepository.countByMessageDateGreaterThanAndEmail(lastLoginDate,person.email).awaitSingle()"Hello${person.name},youhave$numberOfMessagesmessagessince$lastLoginDate"}



目前在Spring应用中使用Kotlin协程还有些小繁琐,但在SpringBoot2.2中,可以直接在SpringWebFlux方法上使用suspend关键字。



Kotlin协程最大的意义就是可以用看似指令式编程方式(ImperativeProgramming

,即传统编程方式)去写异步编程代码。并发和代码可读性似乎两全其美了。



Kotlin协程的局限性

但事情不是那么完美。Kotlin协程依赖于各种基于Callback的技术。像上面的例子,之所以可以用Kotlin协程,是因为上一个版本使用了反应式编程技术。所以,只有当一段代码使用了ListenableFuture、CompletableFuture、ProjectReactor、RxJava等技术时,才能用Kotlin协程进行改造优化。那对于其它的会阻塞线程的技术,如Object.wait、Thread.sleep、Lock、BIO等,Kotlin协程就无能为力了。



另外一个局限性源于Kotlin本身。虽然Kotlin兼容Java,但这种兼容并非完美。因此,对于组件,尤其是基础组件的开发,并不推荐使用Kotlin,而是更推荐使用Java。这也导致Kotlin协程的使用范围被进一步地限制。



六、ProjectLoom

前面讲到,虽然Kotlin协程看上去很好,但在使用上还是有着种种限制。那有没有更好的选择呢?答案是ProjectLoom(https://openjdk.java.net/projects/loom/)。这个项目在18年底的时候已经达到可初步演示的原型阶段。不同于之前的方案,ProjectLoom是从JVM层面对多线程技术进行彻底的改变。



ProjectLoom设计思想与之前的一个开源Java协程技术非常相似。这个技术就是QuasarFiberhttps://docs.paralleluniverse.co/quasar/。而现在ProjectLoom的主要设计开发人员RonPressler就是来自QuasarFiber。



这里建议大家读一下ProjectLoom的这篇文档:http://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html。这篇文档介绍了发起ProjectLoom的原因,以及Java线程基础的很多底层设计。



其实发起ProjectLoom的原因也很简单:长期以来,Java的线程是与操作系统的线程一一对应的,这限制了Java平台并发能力的提升。各种框架或其它JVM编程语言的解决方案,都在使用场景上有限制。例如Kotlin协程必须基于各种Callback技术,而Callback技术有存在编写、调试困难的问题。为了使Java并发能力在更大范围上得到提升,从底层进行改进便是必然。



下面这幅图很好地展示了目前Java并发编程方面的困境,简单的代码并发、伸缩能力差;并发、伸缩能力强的代码复杂,难以与现有代码整合。



为了让简单和高并发这两个目标兼得,我们需要ProjectLoom这个项目。



使用方法

在引入ProjectLoom之后,JDK将引入一个新类:java.lang.Fiber。此类与java.lang.Thread一起,都成为了java.lang.Strand的子类。即线程变成了一个虚拟的概念,有两种实现方法:Fiber所表示的轻量线程和Thread所表示的传统的重量级线程。



对于应用开发人员,使用ProjectLoom很简单:



Fiberf=Fiber.schedule(()->{println("Hello1");lock.lock();//等待锁不会挂起线程try{println("Hello2");}finally{lock.unlock();}println("Hello3");})



只需执行Fiber.schedule(Runnabletask)就能在Fiber中执行任务。**最重要的是,上面例子中的lock.lock()操作将不再挂起底层线程。除了Lock不再挂起线程以外,像SocketBIO操作也不再挂起线程。**但synchronized,以及Native方法中线程挂起操作无法避免。



synchronized(monitor){//在Fiber中调用这条语句还是会挂起线程。socket.getInputStream().read();}



如上所示,Fiber的使用非常简单。因此,让现有系统使用ProjectLoom很容易。像Tomcat、Jetty这样的Web容器,只需将处理请求操作从使用ThreadPoolExecutorexecute或submit改为使用Fiberschedule即可。这个视频https://www.youtube.com/watch?v=vbGbXUjlRyQ&t=1240s中的Demo展示了Jetty使用ProjectLoom改造之后并发吞吐能力的大幅提升。



实现原理

接下来简单介绍一下ProjectLoom的实现原理。ProjectLoom的使用主要基于Fiber,而实现则主要基于Continuation。Contiuation表示一个可暂停和恢复的计算单元。在ProjectLoom中,Continuationn使用java.lang.Continuation类实现。这个类主要供类库实现使用,而不是直接被应用开发人员使用。Continuation主要内容如下所示:



packagejava.lang;publicclassContinuationimplementsRunnable{publicContinuation(ContinuationScopescope,Runnabletarget)publicfinalvoidrun()publicstaticvoidyield(ContinuationScopescope)publicbooleanisDone()}



Continuation实现了Runnable接口,构造时除了需要提供一个Runnable类型的参数以外,还需要提供一个java.lang.ContinuationScope的参数。ContinuationScope顾名思义表示Continuation的范围。Continuation可以被想象成是一个方法执行过程,方法可以调用其它方法。同时,方法执行也有一定的影响范围,如try...catch就规定了相应的范围。ContinuationScope就起到了起到了相应的作用。



Continuation有两个最重要的方法:run和yield。run方法首次被调用时,就会执行Runnabletarget的run方法。但是,在调用了yield方法后,再次调用run方法,Continuation就不会从头执行,而是从yield的位置开始执行。



为了更形象的理解,下面看一个例子:



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

输出结果:

ABC



上面的例子非常简单:创建一个Continuation,其Runnabletarget打印A、B、C,并在其中yield两次。创建之后调用三次run()方法。如果这样执行一个普通的Runnable,那应该打印三次A、B、C,一共打印九次。而Continuation在yield之后执行run,会从yield的位置往后执行,而不是从头开始。



Continuationyield类似Thread的yield,但前者需要显式调用run方法恢复执行。



在ProjectLoom之后,LockSupport的park操作将变为:



publicclassLockSupport{varstrand=Strands.currentStrand();if(strandinstanceofFiber){Continuation.yield(FIBER_SCOPE);}else{Unsafe.park(false,0L);}}



七、展望

Java作为使用率最高的编程软件,在包括后端开发、手机应用开发、大数据等众多领域均有广泛应用。但毕竟是一门诞生20多年的编程语言,存在一些现在看来设计上的不足和受到后来者的挑战都是正常。但必须说明,我们口中的Java并非一门单纯的编程语言。而应该被视为Java语言+JVM+Java类库三部分组成。这三部分中,毫无疑问,JVM是基础。但JVM设计之初就并非和Java语言紧密绑定,紧密绑定的只是字节码。由任何编程语言编译得到的合法字节码都能运行在JVM之上。这使得Java语言层面设计的不足可有其它编程语言解决,于是出现了Groovy、Scala、Kotlin、Clojure等众多JVM语言。这些语言很大程度上弥补了Java的不足。



但像多线程这样的技术,由于和底层虚机和操作系统有千丝万缕的联系,想要彻底改进,绕不开底层优化。这就是ProjectLoom出现的原因。相信ProjectLoom技术会将Java的并发能力提升至和Golang一样的水平,而付出的成本只是对现有项目的少量改动。



Azul的DeputyCTOSimonRitter曾透露ProjectLoom很可能在Java13时发布。究竟能不能赶上Java13,这个不可知,好在Java13的特性还未完全确定,说不定可以ProjectLoom可以赶上末班车。



就算ProjectLoom没能和Java13一起发布。但目前反应式编程的趋势也非常明显。随着新版本的Spring和Kotlin的发布,反应式编程的使用、调试变得越来越简单。Dubbo也明确表示在3.0中将会支持ProjectReactor。R2DBC在不久的未来也会支持MySQL。因此,Java异步编程将快速发展,在易用性方面迅速赶上甚至超过Go。



另一方面,开发人员也不要将自己局限在某种特定技术上,对各种技术都保持开放的态度是开发人员技能不断提高的前提。只会简单说某某语言、某某技术比其它技术更好的技术人员永远不会成为出色的技术人员。



本文主要介绍了关于流是什么意思(Java)的相关养殖或种植技术,综合百科栏目还介绍了该行业生产经营方式及经营管理,关注综合百科发展动向,注重系统性、科学性、实用性和先进性,内容全面新颖、重点突出、通俗易懂,全面给您讲解综合百科技术怎么管理的要点,是您综合百科致富的点金石。
以上文章来自互联网,不代表本人立场,如需删除,请注明该网址:http://23.234.50.4:8411/article/92904.html