什么原因使Java宁愿多线程也不选择协程

首先,我们要总结一下:协同进程是一个很有价值的概念,这将是多任务程序的发展方向。但 Java并没有太多的动机来推动这件事。

回到问题的根源。在我们要介绍的时候,我们要处理的问题是什么?我看无非是这样的:

节约,轻量化,具体来说:

节约内存,每一个执行绪都会有一个堆叠的记忆体,还有核心中的某些资源。

节省了分配线程的代价(每一次都要执行一次系统查询,每次都要执行一次)

节省大量线程切换带来的开销

与 NIO协作,以改善系统吞吐量

使用起来更舒适,更流畅(跑时不同步,写时不同步)

具体来说。

首先是记忆。以 Java Web编程为例,一个 tomcat上的 woker线程池的最大线程数通常会配置为50~500 (目前 springboot的默认值是200)。也就是说,一次只能接收这么多的要求。如果超出了上限,就会被拒绝。假设一个线程为128 KB,那么500个线程所需要的内存就是60多 MB。如果说有什么瓶颈的话,那就是 CPU, IO,带宽, DB等等,那么这一小部分的内存增长,就不算什么了。

上述讨论使 RSS与虚拟机之间的差异变得简单。事实上,当一个线程开始时,它所占用的空间和它的虚拟地址一样大。如果不是在现实中使用,它根本不会占用任何的物理记忆。

而在另一种情况下,比如说即时通讯系统,它要同时处理很多闲置的连接(动辄就是数十万、上百万)。在这种情况下,连接到线程是非常不合算的。但你可以简单地使用 netty来解决这些问题。你可以把 NIO+ wokerthread看作是一组“协程”,只是没有在文法层次上实现,而且书写的时候很难看。问题在于,您的方案是否真的涉及到成千上万、数百万个同时发生的连接?

让我们再来谈谈建立/破坏一个线程所带来的额外开销。在 Java中,使用线程池很好地解决了这个问题。您将会发现,即使您使用 Vert.x或 kotlin的协同程序,您最终还是要依靠线程池来完成任务。goroutine等于是在全球范围内建立了一个“线程池”,而 GOMAXPROCS就是一个最大的线程池;而 Java可以自由地设定多种不同的线程池(比如处理请求一套,异步任务另一套等)。kotlin使用这种方法来建立多种不同的协程范围。这样的话,就显得更有弹性了。

这样就会产生多个线程的交换开销。只有在“活跃”的时候,才会进行线程之间的转换。在网络环境下,由于 IO (Request-Request/Read DB),很多线程都处于被暂停状态,并没有参与操作系统的线程切换。实际上,一台最多200个线程的服务器,同时也只有几十个“活跃线程”。花费并不多。要防止太多的线程交换,需要注意的是,在同一时间内,存在许多“活跃线程”。这一点,我以前在学校的时候,也做过一次。每个结点,每个连结都有一个线程来执行。当模拟开始运行的时候,就有数千个活跃的线程。然后,系统就被卡住了,然后被杀了。

还有就是和 NIO合作的事情。Java的 NIO/Netty/Vert. X/rx Java/Akka都是一个不错的选择。通常情况下,由于 IO的延迟,网蒂就能解决大部分的资源浪费问题。Vert.X/rxJava。可以让程序写的更加“优雅”一点(见仁见智)。Akka就是Java世界里对“原教旨OO“的实现,别具一格。诚然,使用 NIO+完成未来/处理器/lambda并没有像使用 async+ await那样轻松,但至少还能工作。

如果 Java的 NIO确实被用在了业务上,那么 JDBC就是它的核心痛点。这是一个已经存在了数十年的数据库交互协议,它需要用到块 IO。它承载着大量的 Java生态和商业逻辑。Java想要改变它的编程方法,就需要对 JDBC进行重新设计和实现。执行以下操作:https://github.com/vert-x3/vertx-mysql-postgresql-客户机。问题在于,在这个社区中,这样的“异步 JDBC”并不能提供对 Oracle, SQL服务器之类的传统数据库的支持。Mysql和 Postgres这两个选项,还得再加一把火。

如果你仔细地读一读以上那些要求用“协程”来处理的问题,你将会看到,它们可以用不同的方法来处理。如果你认为线程费了很大的力气,你就可以对线程量进行控制,你就可以对线程堆栈进行缩小,你就可以使用线程池来配置最大值和最大值。要使用 go通道,可以使用 disruptor。在 Java的生态中,虽然没有“协程”这样的一级概念,但却有很多工具可以用来解决这些问题。

Java只是不能解决“协同进程“的定义和“优美的书写”的问题。从工程学的观点来看,“写得优雅”的好处并不像许多追求新鲜感的人所认为的那样重要。C#并不是因为它的 ASync await,就把 Java从市场上抢走了。反之,如果 java社区竭尽所能地推动这一点,那么随着协同进程的到来, Java的历史将会发生巨大的变化。想象没有线程和线程局部,@Transactional不起作用了,而且也找不到合适的替代品,你会不会感到沮丧?不管怎么说,这都是一笔不划算的买卖。我不认为甲骨文会对这件事感兴趣。如果有那么多 Java程序员愿意用 OpenJDK,我都不知道该说什么好了。就我所知道的,到了现在,在9012年,仍然有很多Java6的程序员。

而其它一些新语言则没有那么多的历史负担,并且更易于反思“怎样才是多任务编程的现代方法”。kotlin的协程函数, go的 goroutine, JavaScript的同态函数, python的同态函数,以及 Swift的 GCD,都给出了相应的解决方案。如果你真的希望在 Java系统中使用“协程”,那么你可以先从 kotlin入手,因为它是可以进行混合编程的。

多线程之所以会出现 BUG,是因为:

“优先级”的线程切换,你不能决定两个线程的数据访问顺序,完全是随机性的。

“Sync”无法组合,即使组合在一起,也无法组合在一起。

协同程序是否能够避开 BUG,很大程度上取决于能否避开以上两点。如果协同进程的底层使用的是线程池,两个协同进程使用的是共享内存,那么多线程出现的 bug,也会出现在协同进程中。javascript中之所以没有出现这样的 bug,是因为它只有一个用户线程,没有线程之间的转换和同步。“go”是一种用频道制作“goroutine”的电子简报。如果不使用通道,而使用共享的变量,而且不使用同步包进行控制,仍然会出现 bug。

a15fe564e5744316b21291e17b800e95.jpg

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值