目录
一、背景
二、为了提升吞吐性能,我们所做的优化
1. 串行模式
2. 线程池 +Future 异步调用
3. 线程池 +CompletableFuture 异步调用
三、一请求一线程的模型
四、虚拟线程
1. 线程术语定义
2. 虚拟线程定义
3. 虚拟线程创建
4. 虚拟线程实现原理
5. 虚拟线程内存占用评估
6. 虚拟线程的局限及使用建议
7. 虚拟线程适用场景
五、虚拟线程压测性能分析
1. 测试流程
2. 衡量指标
3. Tomcat+普通线程池
4. WebFlux
5. Tomcat+虚拟线程池
六、总结
串行模式
线程池+Future异步调用
线程池+CompletableFuture异步调用
CompletableFuture
对调用流程进行编排,降低依赖之间的阻塞。线程资源浪费瓶颈始终在 IO 等待上
,导致 CPU 资源利用率较低。目前大部分服务是 IO 密集型服务,一次请求的处理耗时大部分都消耗在等待下游 RPC,数据库查询的 IO 等待中,此时线程仍然只能阻塞等待结果返回,导致 CPU 的利用率很低。扩大服务最大线程数
,简单有效,由于存在下列问题,导致平台线程有最大数量限制,不能大量扩充。垂直扩展,升级机器配置,水平扩展,增加服务节点
,也就是俗称的升配扩容大法,效果好,也是最常见的方案,缺点是会增加成本,同时有些场景下扩容并不能 100% 解决问题。采用异步/响应式编程方案
,例如 RPC NIO 异步调用,WebFlux,Rx-Java 等非阻塞的基于 Ractor 模型的框架,使用事件驱动使得少量线程即可实现高吞吐的请求处理,拥有较好的性能与优秀的资源利用,缺点是学习成本较高兼容性问题较大,编码风格与目前的一请求一线程的模型差异较大,理解难度大,同时对于代码的调试比较困难。JDK21 中的虚拟线程可能给出了答案
, JDK 提供了与 Thread 完全一致的抽象 Virtual Thread
来应对这种经常阻塞的情况,阻塞仍然是会阻塞,但是换了阻塞的对象,由昂贵的平台线程阻塞改为了成本很低的虚拟线程的阻塞,当代码调用到阻塞 API 例如 IO,同步,Sleep 等操作时,JVM 会自动把 Virtual Thread 从平台线程上卸载
,平台线程就会去处理下一个虚拟线程,通过这种方式,提升了平台线程的利用率,让平台线程不再阻塞在等待上,从底层实现了少量平台线程就可以处理大量请求,提高了服务吞吐和 CPU 的利用率
。线程术语定义
操作系统线程(OS Thread)
:由操作系统管理,是操作系统调度的基本单位。平台线程(Platform Thread)
:Java.Lang.Thread 类的每个实例,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射。虚拟线程(Virtual Thread)
:一种轻量级,由 JVM 管理的线程。对应的实例 java.lang.VirtualThread 这个类。载体线程(Carrier Thread)
:指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程。虚拟线程定义
平台线程
。平台线程在底层操作系统线程上运行 Java 代码,并在代码的整个生命周期内独占操作系统线程,平台线程实例本质是由系统内核的线程调度程序进行调度,并且平台线程的数量受限于操作系统线程的数量。虚拟线程创建
方法一:直接创建虚拟线程
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("hello wolrd virtual thread");
});
Thread.ofVirtual().unstarted(() -> {
System.out.println("hello wolrd virtual thread");
});
vt.start();
方法三:通过虚拟线程的 ThreadFactory 创建虚拟线程
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt = tf.newThread(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread. ");
});
vt.start();
方法四:Executors.newVirtualThreadPer
-TaskExecutor()
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
System.out.println("Start virtual thread...");
Thread.sleep(1000);
System.out.println("End virtual thread.");
return true;
});
虚拟线程实现原理
virtual thread =continuation+scheduler+runnable
Continuation
实例中:yield
操作进行阻塞,虚拟线程会从平台线程卸载。Continuation.run
会从阻塞点继续执行。Scheduler
也就是执行器,由它将任务提交到具体的载体线程池中执行。Runnable
则是真正的任务包装器,由 Scheduler 负责提交到载体线程池中执行。mount(挂载)
,取消分配平台线程的操作称为 unmount(卸载
)
:mount 操作
:虚拟线程挂载到平台线程,虚拟线程中包装的 Continuation 堆栈帧数据会被拷贝到平台线程的线程栈,这是一个从堆复制到栈的过程。unmount 操作
:虚拟线程从平台线程卸载,此时虚拟线程的任务还没有执行完成,所以虚拟线程中包装的 Continuation 栈数据帧会会留在堆内存中。Continuation
的 yield
操作让出控制权,等待虚拟线程重新分配载体线程并且执行,具体见下面的代码:ReentrantLock lock = new ReentrantLock();
Thread.startVirtualThread(() -> {
lock.lock();
});
// 确保锁已经被上面的虚拟线程持有
Thread.sleep(1000);
Thread.startVirtualThread(() -> {
System.out.println("first");
会触发Continuation的yield操作
lock.lock();
try {
System.out.println("second");
} finally {
lock.unlock();
}
System.out.println("third");
});
Thread.sleep(Long.MAX_VALUE);
}
Continuation
的 yield
操作让出控制权,如果 yield
操作成功,会从载体线程 unmount
,载体线程栈数据会移动到 Continuation 栈的数据帧中,保存在堆内存中,虚拟线程任务完成,此时虚拟线程和 Continuation 还没有终结和释放,载体线程被释放到执行器中等待新的任务;如果 Continuation 的 yield 操作失败,则会对载体线程进行 Park 调用,阻塞在载体线程上,此时虚拟线程和载体线程同时会被阻塞,本地方法,Synchronized 修饰的同步方法都会导致 yield 失败。Continuation#run()
恢复任务执行。Continuation
的 yield
操作进行阻塞。当任务需要解除阻塞继续执行的时候,则调用 Continuation
的 run
恢复执行。--add-exports java.base/jdk.internal.vm=ALL-UNNAMED
可以在本地运行。ContinuationScope scope = new ContinuationScope("scope");
Continuation continuation = new Continuation(scope, () -> {
System.out.println("before yield开始");
Continuation.yield(scope);
System.out.println("after yield 结束");
});
System.out.println("1 run");
// 第一次执行Continuation.run
continuation.run();
System.out.println("2 run");
// 第二次执行Continuation.run
continuation.run();
System.out.println("Done");
Continuation
实例进行 yield
调用后,再次调用其 run
方法就可以从 yield
的调用之处继续往下执行
,从而实现了程序的中断和恢复。虚拟线程内存占用评估
private static final int COUNT = 4000;
/**
* -XX:NativeMemoryTracking=detail
*
* @param args args
*/
public static void main(String[] args) throws Exception {
for (int i = 0; i < COUNT; i++) {
new Thread(() -> {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
Thread.sleep(Long.MAX_VALUE);
}
private static final int COUNT = 4000;
/**
* -XX:NativeMemoryTracking=detail
*
* @param args args
*/
public static void main(String[] args) throws Exception {
for (int i = 0; i < COUNT; i++) {
Thread.startVirtualThread(() -> {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
});
}
Thread.sleep(Long.MAX_VALUE);
}
虚拟线程的局限及使用建议
native
方法或者外部方法
(Foreign Function & Memory API,jep 424 ) 调用不能进行 yield
操作,此时载体线程会被阻塞。当运行在 synchronized
修饰的代码块或者方法时,不能进行 yield
操作,此时载体线程会被阻塞,推荐使用 ReentrantLock。虚拟线程适用场景
Thread-per-request (一请求一线程)风格的应用程序,例如主流的 Tomcat 线程模型或者基于类似线程模型实现的 SpringMVC 框架 ,这些应用只需要小小的改动就可以带来巨大的吞吐提升。
场景一:
在 Spring Boot 中使用内嵌的 Tomcat 去处理 Http 请求,使用默认的平台线程池作为 Tomcat 的请求处理线程池。场景二:
使用 Spring -WebFlux 创建基于事件循环模型的应用程序,进行响应式请求处理。场景三:
在 Spring Boot 中使用内嵌的 Tomcat 去处理 Http 请求,使用虚拟线程池作为 Tomcat 的请求处理线程池 (Tomcat已支持虚拟线程)。测试流程
衡量指标
Tomcat+普通线程池
WebFlux
@Bean
public WebClient slowServerClient() {
return WebClient.builder()
.baseUrl("http://127.0.0.1:8000")
.build();
}
@Bean
public RouterFunction<ServerResponse> routes(WebClient slowServerClient) {
return route(GET("/"), (ServerRequest req) -> ok()
.body(
slowServerClient
.get()
.exchangeToFlux(resp -> resp.bodyToFlux(Object.class)),
Object.class
));
}
Tomcat+虚拟线程池
--enable-preview
,同时 Tomcat 在 10 版本已支持虚拟线程,我们只需要替换 Tomcat 的平台线程池为虚拟线程池即可。@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandler() {
return protocolHandler ->
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}
private final RestTemplate restTemplate;
@GetMapping
public ResponseEntity<Object> callSlowServer(){
return restTemplate.getForEntity("http://127.0.0.1:8000", Object.class);
}
最大的优势就是我们没有修改代码或采用任何反应式技术,唯一更改是将线程池替换为虚拟线程
。虽然改动较小,但与使用线程池相比,性能结果得到了显著改善。一请求一线程的模型
这种方式易于理解和编程实现,也易于调试和性能调优。https://openjdk.org/jeps/444
https://zhuanlan.zhihu.com/p/514719325
https://www.vlts.cn/post/virtual-thread-source-code#%E5%89%8D%E6%8F%90
https://zhuanlan.zhihu.com/p/499342616
往期回顾
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信:
线下活动推荐
主题:得物技术沙龙- SRE稳定性工程探索与实践
时间:2023年11月25日(周六)14:00-18:00
地点:杭州 · 西湖区学院路77号得物杭州研发中心12F
活动亮点:本次得物技术沙龙主题为SRE稳定性工程探索与实践,将在杭州(线上同步直播)为你带来五个令人期待的演讲话题:
《得物C端核心链路稳定性保障实践》
《阿里集团系统稳定性运营管理》
《腾讯游戏SRE在复杂异构业务中的云原生服务实践》
《蚂蚁集团混沌工程技术理论与实践》
《资损防控技术体系简介及实践》
相信这些话题将对你的工作和学习有所帮助,期待与你共同探讨最前沿的技术趋势和实践经验!欢迎线下参与,如果没办法到现场,也可以锁定我们的“得物Tech”视频号哦~
快快点击下面图片报名吧~