在Java最新的LTS版本 21中,终于实装了协程这一特性。当然,在这些诸如python、golang等轻量级语言中被称为协程的东西,在Java中有个全新的代号——虚拟线程,为了将协程与线程做区分,在Java21中,原Thread被称之为平台线程。下文中,将统一使用线程/协程的方式称呼。
相关定义
我们都知道,Java中引入了线程的概念,区别于系统中的进程。作为并发执行的最小单元,在一定的条件下,使用多个线程同时运作可以有效提高程序的运转效率。而线程这一能力源于系统本身而并非JVM。
之所以说是在一定条件下,是因为受限于机器配置情况(CPU的运作机制、核心数),线程的同时运作并不能线性的提升运行性能,单个cpu并不能同时处理多线程任务,实际的运作方式是基于时间片分片,各个线程抢占式执行代码,这样能减少一些无效的io等待(例如网络io、磁盘io实际是会阻塞等待io结果),同时在多核心场景下也能有效利用cpu。但Java的内存区域中存在一些线程独享的空间(栈),所以线程的每次切换都需要重新切换上下文方能继续执行任务,这种切换是有无法被忽略的性能损耗的。
受限于多线程上下文切换的损耗和栈结构较高的内存占用,Java应用在执行多线程任务时,一般需要反复权衡开销,选择合适的并发量(例如某些说法是与cpu核心数为比例去设置,当然这虽然有所依据却并不贴近实际应用)。也因此很多轻量化语言并不直接使用多线程,而使用了协程。
协程本质上是语言内部的一种实现,通过一种近似多路复用的方式模拟多线程的操作和行为,但本质上仍是在单线程内执行逻辑,用底层实现的复杂度换来了更低的内存开销和几乎没有的上下文切换损耗。
前言:多线程并发任务的调优和瓶颈
在web应用中,使用多线程一般是做异步化操作,或是针对io密集型业务进行并发处理避免无效的等待阻塞,提高整体业务逻辑处理效率。在并发处理的场景下,一般会针对单次任务的延迟时间(例如网络请求中的单次请求返回结果的等待时间)、下游承载的吞吐量、服务器资源等因素选择并发的线程数/线程池中的线程数配置。即使在下游请求延迟非常高、下游吞吐量也非常高的场景下,由于上面提到的诸多资源瓶颈,线程数一般最高也是百级(事实上除了
例如在分布式系统中,我们希望通过多线程批量通过HTTP调用下游系统,假定下游系统单次返回时间较久,但吞吐量极高,此情况我们也只能使用百级的线程数去跑批量逻辑。
是银弹?协程的应用场景
先说结论,在已经有多线程的前提下,协程并非银弹,否则Java也不会在21版本才去落地协程。但在某些场景下,使用协程确实能做到一些神乎其技的事。协程的优势在于:
相同数量的协程与线程,协程内存资源消耗更小;
协程免于切换时间片,高并发代价小。
基于此,在以下场景使用Java协程可以获得更好的收益:
高延迟高吞吐任务;
需要创建很多线程并行的任务。