写点什么

为什么 Java 坚持多线程不选择协程?

发布于: 2021 年 03 月 18 日

发现面试时多线程是 Java 绕不去的坎,就有几个问题:


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


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


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


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


所以,学好 Java 中的多线程是否还有必要呢?

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


先返回到问题的本源。当我们希望引入协程,我们想解决什么问题。我想不外乎下面几点:


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


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

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

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

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


3.使用起来更加舒服顺畅(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绕不去的坎,为什么坚持多线程不选择协程呢?


用户头像

还未添加个人签名 2021.03.15 加入

还未添加个人简介

评论

发布
暂无评论
为什么 Java 坚持多线程不选择协程?