优质博文:IT-BLOG-CN
一、简介
虚拟线程是轻量级线程,极大地减少了编写、维护和观察高吞吐量并发应用的工作量。虚拟线程是由JEP 425
提出的预览功能,并在JDK 19
中发布,JDK 21
中最终确定虚拟线程,以下是根据开发者反馈从JDK 20
中的变化:
【1】jdk21
中虚拟线程始终支持线程本地变量。与在预览版本中允许的不同,现在不再可能创建不能具有线程本地变量的虚拟线程。对线程本地变量的有保障支持确保了许多现有库可以不经修改地与虚拟线程一起使用,并有助于将以任务为导向的代码迁移到使用虚拟线程。
【2】直接使用Thread.Builder API
创建的虚拟线程(而不是通过Executors.newVirtualThreadPerTaskExecutor()
创建的虚拟线程)现在默认情况下也会在其生命周期内进行监控,并且可以通过描述在"观察虚拟线程"部分中的新线程转储来观察。
基于协程的线程,与其他语言中的协程有相似之处,也有不同。虚拟线程是依附于主线程的,如果主线程销毁了,虚拟线程也不复存在。
二、背景
1、大量应用时同步方式,修改成异步方式投入资源大;
2、由线程池被打满引起的事故很难杜绝,很多应用将核心和非核心的应用一起交由线程池管理;
解决上面问题有两种措施:
1、NIO:优点是有成熟框架Reactor
、RxJava
等。缺点是可读性欠缺,改造难度大;
2、虚拟线程:优点是业务侧改造成本低,无需池化,天然隔离。缺点是对native
、synchronize
方法或者外部函数不友好;
三、原理
调度方式: 当前线程将任务提交给虚拟线程的时候,是一个Runnalbe
状态,存放在队列中排队。任务排到第一位后,会挂在到平台线程上Platform Thread
,该线程就是用户线程New Thread
的线程。当任务挂载上去之后,就是一个运载线程,执行虚拟线程中的任务。当线程执行到阻塞或者IO
操作的时候,它会将当前任务卸载到队列中,重新编程Runnable
状态。
状态机: 与平台的线程的状态相似,我们主要看下如下两个状态的变化
RUNNING -> PARKING | 与普通线程一致,通常由各种block导致 | 触发后置为PARKING状态,卸载虚拟线程,调用Continuation.yield()方法 |
---|---|---|
RUNNING -> YIELDING | 通常为IO阻塞时 | 置为YIELDING状态,卸载虚拟线程,调用Thread.yield |
四、使用场景
计算密集型
CPU机密型: 并行开启X个任务,每个任务对5W个随机数进行排序;
结论:虚拟线程对于CPU
密集型应用无优势
IO密集型
并发数 | CPU | 响应时间(ms) | 吞吐量 |
---|---|---|---|
10 | 35 | 19 | 490 |
20 | 55 | 20 | 925 |
30 | 80 | 22 | 1296 |
40 | 95 | 26 | 1468 |
50 | 99 | 32 | 1508 |
结论:虚拟线程在CPU
使用率达到80以后,性能有些许衰退。
结论: 相同的并发下
1、由于虚拟线程不需要大量的系统线程调度,节省了CPU
的开销;
2、系统线程的大量减少,减少了CPU_Load
排队的情况;
3、虚拟线程替换了dal
的线程池,减少了线程数量(上面包含了JVM自身的线程和框架的线程);
使用案例代码:
CDubboClient instance = CDubboClient.getInstance();
FlightPassengerWS service = instance.getService(FlightPassengerWS.class);
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
StudentResponseType studentResponse = executor.submit( () -> service.getStudent(request).get());
}
五、死锁
synchronize
同步代码块导致的死锁现象:
结论: 虚拟线程获取了连接后IO
发生了卸载,当链接数耗尽,装载状态的虚拟线程由于拿不到链接被BLOCK
,发生yield
。由于在同步代码块中,yield
失败发生绑定。导致其他获取链接的虚拟线程无运载线程可用。
解决办法: 使用ReentrantLock
替换Synchronized
private final ReentranLock synLock = new ReentranLock();
synchronized(this) {
}
// 替换为
synLock.lock();
try {
} finally {
synLock.unlock();
}
六、实践
QPS 1000+ 的项目性能监控
平台线程 | 虚拟线程 | |
---|---|---|
CPU使用率(avg) | 20% | 15% |
CPU_Load(max) | 15 | 5.5 |
线程数(avg) | 260 | 258 |
时间响应(avg) | 118ms | 97ms |
P99.9 | 3460ms | 1676ms |
使用虚拟线程后,由于切换了线程,无法从HttpContext.current()
获取到任何信息,需要在虚拟线程里threadlocal
重新set
。