前言
众所周知,java 是没有协程线程的,在我们如此熟知的jdk 1.8时代,大佬们想出来的办法就是异步io,甚至用并行的stream流来实现,高并发也好,缩短事件处理时间也好;大家都在想着自己认为更好的实现方式;
在来说说吧,我为什么会在今天研究这个破b玩意儿呢,
这事情还的从一个月前的版本维护说起,
目前公司游戏运营的算中规中矩吧,日新增和日活跃用户基本保持在1w,2.5w样子;
大概1-2周会有一次版本更新,需要停服维护的,
我想大部分做游戏的同僚可能都知道,游戏架构里面包含一个登录服这么一个环节,用于对账号管理以及和sdk平台做登录二次验证;
我们的问题也就出在了这sdk二次登录验证环境;

从这个截图中不难看出,我在向sdk服务器进行验证的时候http请求耗时,一个请求多长达400ms,按照这个逻辑,一个线程一秒钟也只能是2个登录;
然后面对停服维护阶段,玩家疯狂的尝试登录,导致登录服务器直接积压了30万个登录请求等待处理;
在寻求方案的时候,看到了http请求池化方案,目前已经大线程池(这里是本人自定义线程池)和http池化(基于 Apache CloseableHttpClient)处理方案 因为平台是jdk11的
在寻求方案同时发现了jdk19开放的预览版新功能虚拟线程;翻阅了一些资料,就像这虚拟线程能不能为我带来更好性能体验,让现有的系统,吞吐量更上一层楼;
一下测试代码用的是jdk20测试
构建虚拟线程
第一步我们需要先创建虚拟线程,才能去理解什么是虚拟线程
- 1 public static void main(String[] args) throws Exception {
- 2
- 3 Thread.startVirtualThread(() -> {
- 4 System.out.println(Thread.currentThread().toString());
- 5 });
- 6
- 7 Thread.sleep(3000);
- 8 }

这就正确的启动了一个虚拟线程;从线程明明输出看着是不是有点眼熟,是不是跟stream的并行流很相似;
接下来我们看看虚拟线程的运行是怎么回事,

- 1 public static void main(String[] args) throws Exception {
- 2
- 3 Thread.startVirtualThread(() -> {
- 4 try {
- 5 Thread.sleep(5000);
- 6 } catch (InterruptedException e) {
- 7 throw new RuntimeException(e);
- 8 }
- 9 System.out.println(Thread.currentThread().toString());
- 10 });
- 11
- 12 Thread.startVirtualThread(() -> {
- 13 try {
- 14 Thread.sleep(5000);
- 15 } catch (InterruptedException e) {
- 16 throw new RuntimeException(e);
- 17 }
- 18 System.out.println(Thread.currentThread().toString());
- 19 });
- 20 Thread.startVirtualThread(() -> {
- 21 try {
- 22 Thread.sleep(5000);
- 23 } catch (InterruptedException e) {
- 24 throw new RuntimeException(e);
- 25 }
- 26 System.out.println(Thread.currentThread().toString());
- 27 });
- 28 Thread.startVirtualThread(() -> {
- 29 try {
- 30 Thread.sleep(5000);
- 31 } catch (InterruptedException e) {
- 32 throw new RuntimeException(e);
- 33 }
- 34 System.out.println(Thread.currentThread().toString());
- 35 });
- 36 Thread.startVirtualThread(() -> {
- 37 try {
- 38 Thread.sleep(5000);
- 39 } catch (InterruptedException e) {
- 40 throw new RuntimeException(e);
- 41 }
- 42 System.out.println(Thread.currentThread().toString());
- 43 });
- 44 Thread.sleep(3000);
- 45 }
View Code
我们多new几个虚拟线程来看看监控

看到了吧,实际上你new的虚拟线程,其实是被当成了一个任务丢到了线程池里面在运行;

在翻阅了现有的代码逻辑还不能定义这个底部线程池,只能使用默认的;
当然目前是预览版,不确定之后会不会可以自定义实现,stream流一样,可以定义它并行数量;
线程池对比
测试用例1

- 1 @Test
- 2 public void r() {
- 3 t1();
- 4 t2();
- 5 }
- 6
- 7 public void t1() {
- 8 AtomicInteger atomicInteger = new AtomicInteger(100);
- 9 try (var executor = Executors.newFixedThreadPool(10)) {
- 10 long nanoTime = System.nanoTime();
- 11 for (int i = 0; i < 100; i++) {
- 12 executor.execute(() -> {
- 13 try {
- 14 Thread.sleep(50);
- 15 } catch (InterruptedException e) {
- 16 throw new RuntimeException(e);
- 17 }
- 18 atomicInteger.decrementAndGet();
- 19 });
- 20 }
- 21 while (atomicInteger.get() > 0) {}
- 22 System.out.println("平台线程 - " + atomicInteger.get() + " - " + ((System.nanoTime() - nanoTime) / 10000 / 100f));
- 23 }
- 24 }
- 25
- 26 public void t2() {
- 27 AtomicInteger atomicInteger = new AtomicInteger(100);
- 28 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
- 29 long nanoTime = System.nanoTime();
- 30 for (int i = 0; i < 100; i++) {
- 31 executor.execute(() -> {
- 32 try {
- 33 Thread.sleep(50);
- 34 } catch (InterruptedException e) {
- 35 throw new RuntimeException(e);
- 36 }
- 37 atomicInteger.decrementAndGet();
- 38 });
- 39 }
- 40 while (atomicInteger.get() > 0) {}
- 41 System.out.println("虚拟线程 - " + atomicInteger.get() + " - " + ((System.nanoTime() - nanoTime) / 10000 / 100f));
- 42 }
- 43 }
View Code

通过这段测试代码对比,总任务耗时,显而易见性能;
测试用例2
- 1 public void t2p() {
- 2 Runnable runnable = () -> {
- 3 long g = 0;
- 4 for (int i = 0; i < 10000; i++) {
- 5 for (int j = 0; j < 10000; j++) {
- 6 for (int k = 0; k < 100; k++) {
- 7 g++;
- 8 }
- 9 }
- 10 }
- 11 };
- 12 AtomicInteger atomicInteger = new AtomicInteger(100);
- 13 try (var executor = Executors.newFixedThreadPool(10)) {
- 14 long nanoTime = System.nanoTime();
- 15 for (int i = 0; i < 100; i++) {
- 16 executor.execute(() -> {
- 17 runnable.run();
- 18 atomicInteger.decrementAndGet();
- 19 });
- 20 }
- 21 while (atomicInteger.get() > 0) {}
- 22 System.out.println("平台线程 - " + atomicInteger.get() + " - " + ((System.nanoTime() - nanoTime) / 10000 / 100f));
- 23 }
- 24 }
- 25
- 26 public void t2v() {
- 27 Runnable runnable = () -> {
- 28 long g = 0;
- 29 for (int i = 0; i < 10000; i++) {
- 30 for (int j = 0; j < 10000; j++) {
- 31 for (int k = 0; k < 100; k++) {
- 32 g++;
- 33 }
- 34 }
- 35 }
- 36 };
- 37 AtomicInteger atomicInteger = new AtomicInteger(100);
- 38 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
- 39 long nanoTime = System.nanoTime();
- 40 for (int i = 0; i < 100; i++) {
- 41 executor.execute(() -> {
- 42 runnable.run();
- 43 atomicInteger.decrementAndGet();
- 44 });
- 45 }
- 46 while (atomicInteger.get() > 0) {}
- 47 System.out.println("虚拟线程 - " + atomicInteger.get() + " - " + ((System.nanoTime() - nanoTime) / 10000 / 100f));
- 48 }
- 49 }

通过测试用例2不难看出,虚拟线程已经不占优势;
这是为什么呢?
总结
平台线程我就不过多描述因为大家都知道,网上的描述也特别多;
虚拟线程,其实我们更多可以可以考虑他只是一个任务,异步的任务;
区别在于,平台线程受制于cpu,如果你执行任务很耗时或者比如网络io等挂起等待,那么这个cpu也会一直挂起等待无法处理其他事情;
虚拟线程是异步任务凌驾于平台线程之上,也就是说,当你的虚拟线程等待挂起的时候,平台线程就去执行其他任务(其他虚拟线程)去了
我们通过上面测试用例可以这样理解,
用例1,通常我们的RPC服务或者SDK跟我开通SDK二次验证大部分时间处于等待挂起业务,这时候虚拟线程的作用就会非常大,他可以发起大量的验证请求,等待回答;我们通常定义的IO密集型应用;
用例2,属于计算型的,它会一直占用cpu时间片,不会腾出cpu去执行其他事件;我们通常说cpu密集型应用不太适用虚拟线程;
目前虚拟线程的执行依赖于底层线程池,我们无法自主控制它,所以不是很建议使用
关于虚拟线程的描述或者定义我就不在过多的去阐述,
我只说一下它运行的逻辑吧,
1,在不同时间段一个虚拟线程可以由不同的平台线程调度,也可以由一个平台线程调度,平台线程=系统线程=cpu
2,在不同时间段一个平台线程在可以调度不同的虚拟线程,也可以反复调度一个虚拟线程
3,在同一时间段,一个平台线程只能调用一个虚拟线程,一个虚拟线程只能由一个平台线程调度
换言之,其实虚拟线程可以看成一个task,你可以new很多的task,至于他什么时候被执行,就看你的工人(cpu)什么时候有空,

View Code
附加一段全部测试代码

