【深度探讨】为什么 Java 坚持多线程,而不选择协程呢?

问题的本源

当我们希望引入协程,我们想解决什么问题。我想不外乎下面几点:

  • 节省资源,轻量,具体就是:

  • 节省内存,每个线程需要分配一段栈内存,以及内核里的一些资源

  • 节省分配线程的开销(创建和销毁线程要各做一次 syscall)

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

  • 与 NIO 配合实现非阻塞的编程,提高系统的吞吐

  • 使用起来更加舒服顺畅(async+await,跑起来是异步的,但写起来感觉上是同步的)

我们分开来讲下。

先说内存。拿 Java Web 编程举例子,一个 tomcat 上的 woker 线程池的最大线程数一般会配置为 50~500 之间(目前 springboot 的默认值给的 200)。也就是说同一时刻可以接受的请求最多也就是这么多。如果超过了最大值,请求直接打失败拒绝处理。假如每个线程给 128KB,500 个线程放一起的内存占用量大概是 60+MB。如果真的有瓶颈,也许 CPU,IO,带宽,DB 的 CPU 等会有瓶颈,但这点内存量的增幅对于动辄数个 GB 的 Java 运行时进程来说似乎并不是什么大问题。

上面的讨论简化了 RSS 和 VM 的区别。实际上一个线程启动后只会在虚拟地址上占位置那么多的内存。除非实际用上,是不会真的消耗物理内存的。

换一个场景,比如 IM 服务器,需要同时处理大量空闲的链接(可能要几十万,上百万)。这时候用 connection per thread 就很不划算了。但是可以直接改用 netty 去处理这类问题。你可以理解为 NIO + woker thread 大致就是一套 “协程”,只不过没有实现在语法层面,写起来不优雅而已。问题是,你的场景真的处理了并发几十万,上百万的连接吗?

再说创建 / 销毁线程的开销。这个问题在 Java 里通过线程池得到了很好的解决。你会发现即便你用 vert.x 或者 kotlin 的协程,归根到底也是要靠线程池工作的。goroutine 相当于设置一个全局的 “线程池”,GOMAXPROCS 就是线程池的最大数量;而 Java 可以自由设置多个不同的线程池(比如处理请求一套,异步任务另外一套等)。kotlin 利用这个机制来构建多个不同的协程 scope。这看起来似乎会更灵活一点。

然后是线程的切换开销。线程的切换实际上只会发生在那些 “活跃” 的线程上。对于类似于 Web 的场景,大量的线程实际上因为 IO(发请求 / 读 DB)而挂起,根本不会参与 OS 的线程切换。现实当中一个最大 200 线程的服务器可能同一时刻的 “活跃线程” 总数只有数十而已。其开销没有想象的那么大。为了避免过大的线程切换开销,真正要防范的是同时有大量 “活跃线程”。这个事情我自己上学的时候干过,当时是写了一个网络模拟器。每一个节点,每一个链路都由一个线程实现。模拟跑起来后,同时的活跃线程上千。当时整个机器瞬间卡死,直到 kill 掉这个程序。

此外说说与 NIO 的配合。在 Java 这个生态里 Java NIO/Netty/Vert.X/rxJava/Akka 可以任意选择。一般来讲,Netty 可以解决绝大部分因为 IO 的等待造成资源浪费的问题。Vert.X/rxJava。可以让程序写的更加 “优雅” 一点(见仁见智)。Akka 就是 Java 世界里对“原教旨 OO“的实现,很有特色。的确,用 NIO + completedFuture/handler/lambda 不如 async+await 写起来舒服,但起码是可以干活的。

如果真的要较真 Java 的 NIO 用于业务的问题,其核心痛点应该是 JDBC。这是个诞生了几十年的,必须使用 Blocking IO 的 DB 交互协议。其上承载了 Java 庞大的生态和业务逻辑。Java 要改自己的编程方式,必须得重新设计和实现 JDBC,就像 https://github.com/vert-x3/vertx-mysql-postgresql-client 那样做。问题是,社区里这种 “异步 JDBC” 还没有支持 oracle、sql server 等传统 DB。对 mysql 和 postgres 的支持还需要继续趟坑~

如果认真阅读上面这些需要 “协程” 解决的问题,就会发现基本上都可以以各种方式解决。觉得线程耗资源,可以控制线程总数,可以减少线程 stack 的大小,可以用线程池配置 max 和 min idle 等等。想要 go 的 channel,可以上 disruptor。可以说,Java 这个生态里尽管没有 “协程” 这个第一级别的概念,但是要解决问题的工具并不缺。

Java 仅仅是没有解决” 协程 “在 Java 中的定义,以及 “写得优雅 “这个问题。从工程角度,“写得优雅” 的优势并没有很多追新的人想象的那么关键。C# 也并非因为有了 async await 就抢了 Java 的市场分毫。而反过来,如果 java 社区全力推进这个事情,Java 历史上的生态的积累却因为协程的出现而进行大换血。想像一下如果没有 thread,也没有 ThreadLocal,@Transactional 不起作用了,又没有等价的工具,是不是很郁闷?这么看来怎么着都不是个划算的事情。我想 Oracle 对此并不会有太大兴趣。OpenJDK 的 loom 能不能成,如果真的 release 多少 Java 程序员愿意使用,师母已呆。据我所知在 9012 年的今天,还有大量的 Java6 程序员。

其他新的语言历史包袱少,比较容易重新思考 “什么是现代的 multi-task 编程的方式 “这个大主题。kotlin 的协程、go 的 goroutine、javascript 的 async await、python 的 asyncio、swift 的 GCD 都给了各自的答案。如果真的想入坑 Java 这个体系的 “协程”,就从 kotlin 开始吧,毕竟可以混合编程。

多线程容易出 bug 的主要原因

主要因为:

  • “抢占 “式的线程切换 —— 你无法确定两个线程访问数据的顺序,一切都很随机

  • “同步 “不可组装 —— 同步的代码组装起来也不同步,必须加个更大的同步块

协程能不能避免容易出 bug 的缺陷,主要看能不能避免上面两个问题。如果协程底层用的还是线程池,两个协程还是通过共享内存通讯,那么多线程该出什么 bug,多协程照样出。javascript 里不出这种 bug 是因为其用户线程就一个,不会出现线程切换,也不用同步;go 是建议用 channel 做 goroutine 的通讯。如果 go routine 不用 channel,而是用共享变量,并且没有用 Sync 包控制一下,还是会出 bug。

为什么多线程在 Java 中这么重要?

从 java 被发明的第一天起,就被定义为一个多线程的网络编程语言。Java 最大特点并不是跨平台,而是它的多线程模型(那时候的 C++ 中,并没有我们现在看到的 thread,C# 还没有出来)。因为近二十年的软件行业的增长主要来自网络编程,网络编程最常见的模型就是 client/server, 也就是所谓的 C/S,这种编程模型在服务器端需要同时接受客户端的请求,也就是说要有很好的并发特性 -- 这个特性主要依赖多线程来实现。而 java 的主战场就是服务器端编程。所以多线程对 java 是极为重要,不可或缺的一环。

多线程会出现难以排查的 BUG,那么使用协程的话能否避免这些 BUG 呢?

多线程 debug 的困难主要来自于运行时的多个线程的抢先式调度,我们可以观察到即便是只有一个 cpu 核心,多个线程也会互相随机插入(inter-weaving), 这造成了运行过程和结果的不确定性(non-deterministic)。同样一个程序,同样的输入,每次运行会有不用的运行轨迹(path),出现不同的结果。这个问题并非不能解决,我们可以采用 model checking 等工具检验所有程序可能执行到的 path,这个话题就不扯的太远了。

最早的协程(这要追朔到几十年前,其实协程这种方式是早于多线程的)是可以很大程度避免这种问题的。因为协程的特点在于 “协”,有一个主动让出的机制。我不用了才让给你。而现在主流操作系统多线程的特点在于“抢”,出现了我前面提到的不确定性。但现在我们常见的这些叫“协程” 的技术的底层还是多线程。比如说 go 的协程在运行时,还是分配一个线程给它,一般来说,go 使用的并发协程 / 线程数等于 cpu 的核心数,在多核的环境下,多线程的问题(比如说 race condition,dead lock 等等)协程也一样会有。

go 的协程是可以跑满整个核心的,但 Java 是不是除非从语言底层改造,否则做不到这一点?

java 一样可以跑满所有的核心,你自己或者所用的框架多起一些线程就好了,现实中多数 java 服务器端应用对 cpu 的利用是很到位的。

Kotlin 支持协程,是否用起来比多线程好呢?

Kotlin 的协程带来了很多好处,比如说让开发更简单。但 Java 世界有很多已有框架一样可以让多线程开发变得简单。至于协程和线程调度算法的差异,以及调度算法所带来的性能的差异,就要根据应用场景进行具体分析了,其实很多时候 “协” 造成了资源让出不及时,反而提高了系统的延时,不能一概而论。

学习和使用协程是不是很有好处呢?

是的,协程这种并发模型非常适合大多数的网络编程需要,可以很简单的写出高性能的服务器端应用。是非常值得大家学习的一项技术。

结论

协程是非常值得学习的概念,它是多任务编程的未来。但是 Java 全力推进这个事情的动力并不大。

【限时福利】公众号回复"A123"


作者:大宽宽  和  北南

链接:

https://www.zhihu.com/question/332042250/answer/734115120

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值