线程的困境
随着业务量的增长和为了应对复杂业务进行的服务细分,现代B/S系统完成一次外部业务请求的响应往往需要分布在不同机器上的大量服务协同完成,这样做在降低单个服务复杂度、提高复用性的同时,也增加了服务的数量,缩短了留给每个服务的响应时间。
目前的要求:
- 每一个服务需要在极短时间内完成,这样完成一次响应的总时长才不会过长
- 每一个服务提供者都要能同时响应数量更庞大的请求,这样才不会因某个服务被阻塞而整个请求等待。
目前的问题:
- Java线程的主要实现方式是1:1线程模型,采用内核线程实现,这样导致线程的切换、调度成本高昂,甚至线程调度的开销会接近于单个服务运算的开销,造成时间的严重浪费。
- 采用内核线程实现,就会占用内核资源,因此内核线程数目有限,难以响应数目庞大的请求。
- 在之前的单个应用场景下,采用内核线程,可以在一个线程内完成业务的处理,此时线程切换的开销就相对较小。
内核线程的开销
内核线程的调度成本主要来自于内核态与核心态的状态。
阶段 | 单极中断 | 多级中断 |
---|---|---|
中断隐指令 | 关中断 | 关中断 |
保存断点PC | 保存断点 | |
送中断向量(中断服务程序入口地址) | 送中断向量 | |
中断服务程序 | 保护现场 | 保护现场和屏蔽字 |
— | 开中断 | |
执行中断服务程序 | 执行中断服务程序 | |
— | 关中断 | |
恢复现场 | 恢复现场和屏蔽字 | |
开中断 | 开中断 | |
中断返回 | 中断返回 |
每次线程的切换都需要通过中断切入内核态进行程序上下文的保存和恢复,此时涉及到大量的拷贝操作,加大了线程的开销。
协程
由于最初用户线程多用协同式调度,因此该类线程被称为协程:
- 有栈协程:协程会完整地做调用栈的保护和恢复工作
- 无栈协程:本质上是一种有限状态机,状态保存在闭包中,比有栈协程轻量,但功能受限
无论是有栈协程、还是无栈协程,都比传统内核线程轻量得多。以Linux上的HotSpot虚拟机为例,线程栈默认为1MB,而且内核数据结构还要占用16KB。而协程的栈通常仅有几百字节到几KB,大大降低了上下文保存和恢复的时间开销,而且均在用户态完成,不需要切换到内核态。
HotSpot虚拟机把虚拟机栈和本地方法栈合二为一,并没有进程明显区分,如何协程中调用本地方法,还能够正常切换协程而不影响整个线程?协程遇到传统的线程同步应如何做?
Java的解决方法
2018年,OpenJDK创建了Loom项目,用于设计纤程,目前仍未完成。
Oracle官方解释
- 一种轻量级或用户线程,被JVM调度,而不是被操作系统调度
- 纤程是轻量级的,而且切换开销低,并发数目大
待纤程开发完成后,Java将会呈现两种并发模型的开发方式,可以在程序中同时使用。
采用纤程的代码被分为执行过程(Continuation)和调度器(Scheduler):
- 执行过程用来维护执行现场,保护、恢复上下文状态
- 调度器负责编排所有要执行的代码