JDK21可能是java世界近10年(也可能是20年)最重要的更新,虚拟线程的出现让各类java的开发模式都会发生变化,几乎接近一种新语言。
本文讨论在虚拟线程的原理以及其对gc的影响。
虚拟线程
先测试一下:
Thread.ofVirtual().start(() -> {
System.out.println("virtual");
}).join();如果在空项目中测试,一定记得join
日常代码中怎么创建虚拟线程
Thread.ofVirtual().start(() -> System.out.println("virtual"));像golang使用go一样,切记不要像过去一样使用线程池,虚拟线程并不需要复用。
虚拟线程执行在什么真实线程中?
在java.lang.VirtualThread代码中可以看到,最终还是
return new ForkJoinPool(parallelism, factory, handler, asyncMode,
0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);因此在jvm层面只能看到几个ForkJoinPool,数量由Runtime.getRuntime().availableProcessors()或-Djdk.virtualThreadScheduler.parallelism决定
虚拟线程的本质是什么?
在同个线程中顺序的执行代码,当遇到io密集任务时,交出控制权到其他代码中,因此本质上来说:
- 看起来像是并发,体验也是并发,实际并不是(在同个进程中时)
- 对cpu密集型运算没有任何帮助,对于对业务开发更常见的io密集型有很大帮助
- 因为在同个线程中,没有对栈内存的使用,都在使用堆内存,因此对gc有更大的压力
- 极大的降低了开发及debug成本
虚拟线程+gc
虚拟线程把其栈放在堆中,不像普通线程放在栈中。大量创建线程会产生cpu压力,而大量创建虚拟线程会产生gc压力,虽然每个虚拟线程实际上只有一个java.lang.Thread对象+部分信息的大小(几百bytes)。
测试方法:
使用更偏向io密集的方式进行测试,1000并发在堆内创建大量的对象。只有nio比较能接近虚拟线程的场景,1000个普通线程在真实场景中几乎不可能出现。
因此使用1000个mono循环调用自身 vs 1000个虚拟线程while(true)死循环运行,两者背后都是真实的10个线程,http get请求一个1M的文件,放在本机的nginx下。1m的String对象在大并发的情况下会给gc造成很大压力,以此来测试gc性能。
非常简单的虚拟线程代码:
IntStream.range(0, 1000).forEach(j -> Thread.ofVirtual().start(() -> {
while(true) {
http.Do(j);
}
}));比较晦涩的reactor代码:
public void run(String... args) throws Exception {
IntStream.range(0, 1000).forEach(i -> Do());
}
void Do(){
webClient.get().uri("/1m.json").retrieve().bodyToMono(String.class).doOnSuccess((s) -> {
Do();
}).subscribe();
}java21最新的分代zgc
使用参数:-XX:+UseZGC -XX:+ZGenerational -Xmx4g
zgc是没有年轻代老年代的,因此它的gc动作是发生在所有堆内存上的。分代zgc把分代这个传统模式又加回了zgc,毕竟再快的gc,运行在更小的范围中也会更快。
⬆️ 虚拟线程
⬆️ reactor
可以看到reactor模式的内存明显要低大概30%,虚拟线程还是比较耗费内存
普通zgc
⬆️ 虚拟线程
⬆️ reactor
可以看到reactor模式的内存会稍微低一些,但不太明显。这也是为什么分代zgc会在将来全面替换普通zgc的原因,分代后效率还是更高
默认的g1
⬆️ 虚拟线程
⬆️ reactor
g1似乎非常不适应虚拟线程,几乎满载的状态,很担心快要oom了。使用虚拟线程一定不要使用g1
结论:
虚拟线程使用要点:
- 不要使用线程池
- 尽量不要使用ThreadLocal。虽然可以正常运行,但创建太快又不能复用,会给ThreadLocal造成很大压力。这里golang的channel是比较好的方案,希望java世界在虚拟线程的普及中也有类似的方案出来
- 不要使用synchronized,因为其会锁住真实的ForkJoinPool造成协程调度失败。使用ReentrantLock
- 必须使用分代zgc,否则gc压力比过去大很多
jdk 8-17的gc对比:
a |