Java 语言抽象和隐藏了各种操作系统线程差异性的接口,这曾经是它区别于其他编程语言的一大优势,但在某些场景下,却已经出现了疲态;
文章目录
- 1. 内核线程的局限
- 2. 协程的复苏
- 3. Java 的解决方案
1. 内核线程的局限
在微服务架构中,要求每个服务提供者可以同时处理数量庞大的请求,而不出现由某个服务被阻塞而整体等待;
Java 目前的并发编程机制(内核线程实现)与此存在矛盾,映射到操作系统上的线程的切换、调度成本高昂(线程切换开销可能接近于计算本身的开销),系统能容纳的线程数量有限;
线程 A -> 系统中断 -> 线程 B
从线程 A 切换到线程 B 之前,操作系统首先需要把现场 A 的上下文数据妥善保管,让后把寄存器、内存分页等恢复到线程 B 挂起时的状态;这种保护与恢复现场的工作涉及一系列寄存器、缓存的来回拷贝,不可能是一种轻量级的操作;
2. 协程的复苏
栈纠绕
(Stack Twine
),有用户自己模拟的多线程、自己保护恢复现场的工作模式;通过内存划分额外空间模拟调用栈,让现场中的方法压栈、退栈遵守规则;协程
(Coroutine
),被设计成协同式调度(Cooperative Scheuling)的用户线程;有栈协程
(Stackfull Coroutine
),会完整地做调用栈的保护、恢复工作;无栈协程
(Stackless Coroutine
),有限状态机,状态保存在闭包里,比有栈协程更轻量,功能也更有限;(await、async、yield 关键字应用);
64 位 Linux 上 HotSpot 的线程栈容量默认是 1 MB,内核数据结构额外消耗 16KB 内存,而一个协程的栈通常在几百字节到几 KB;可见 JVM 线程容量在 200 左右,而协程容量可以到十万级;
Java 调用栈与本地调用栈是在一起的,在调用本地方法时切换协程,可能影响整个线程;一旦遭遇 synchronized 关键字,挂起的仍是整个线程;
3. Java 的解决方案
纤程
(Fiber
),一种有栈协程;A light weight or user mode thread, scheduled by the Java virtual machine, not the operating system; Fibers are low footprint and have negilgible task-switching overhead. You can have millions of them!
Fiber 与目前线程模型保持相似的 API,目标是与内核线程实现共存;
5000 QPS,400 线程 vs. 纤程,线程 latency 在 10s ~ 20s,而纤程 latency 在 200ms;
纤程并发代码会被分为两个部分:执行过程(Continuation,维护执行现场,保护、恢复上线文状态)和调度器(Scheduler,编排所有要执行的代码的顺序,Loom 的默认调度器是 Fork/Join 池);
可以使用 Quasar 协程库独立实现协程调度;但由于其实现方式是通过字节码注入采用局部变量保存和恢复上下文,存在较大性能问题;且要求用户手动标准每一个需要使用协程的函数,对即时编译器的干扰也较大;
上一篇:「JVM 高效并发」Java 线程
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》