聊聊 Java 中的虚拟线程 VirtualThread
Java 中的虚拟线程 VirtualThread 从JDK 19开始引入的预览新特性,现在JDK已经发展到JDK 23了,虽然现在很多企业还是秉持着“你发任你发,我用Java8”的态度,但是作为一个Java程序员,还是要了解一下Java的新特性,毕竟Java是一个不断发展的语言,不断的更新迭代,我们也要跟着时代的步伐,不断的学习,不断的进步。
好的,言归正传,我们来聊聊 Java 中的虚拟线程 VirtualThread。在说 VirtualThread 之前,我们先了解一下 线程模型 ,也是对应我们的 传统线程 和 虚拟线程 的理论基础。
线程模型
什么是线程模型?线程模型是指 用户线程 和 内核线程 之间的关系,也就是 用户线程 和 内核线程 是如何对应(映射)的。在Java中,我们通常使用的那些Thread对象里线程,其实是 用户线程,而 内核线程 是由操作系统来管理的,Java中的线程是由JVM来管理的,JVM会将Java中的线程映射到操作系统的线程上,这个映射关系就是 线程模型。
(一):1-1(一个用户线程对应于一个系统(内核)线程)
1对1的线程模型,即一个用户线程对应一个系统(内核)线程,
优点: 这种模型的优点是实现简单;
缺点: 系统(内核)线程资源有限,如果一个应用程序中有大量的线程,那么系统(内核)线程的创建和销毁的开销大时影响系统性能。
图示:
(二)N-1(N个用户线程对应于一个系统(内核)线程)
N对1的线程模型,即N个用户线程对应一个系统(内核)线程,
优点: 这种模型的优点是节省了系统(内核)线程的创建和销毁的开销;
缺点: 结构相较于一对一模型较为复杂,有可能出现部分线程过忙,部分线程过闲的情况。
图示:
(三):N-M(N个用户线程对应于M个系统(内核)线程)
N对M的线程模型,即N个用户线程对应M个系统(内核)线程,
优点:
1:这种模型的优点是节省了系统(内核)线程的创建和销毁的开销;
2:一个用户线程可以对应多个系统(内核)线程,避免有的线程过忙或过闲;
缺点: 结构是这个三种模型中最复杂的,实现难度较大。
图示:
Java虚拟机使用的线程模型
我们把之前直接使用new Thread()创建的线程成为 平台线程,而 VirtualThread 称为 虚拟线程;
(一):平台线程
平台线程使用的1对1的线程模型,即一个传统线程对应一个系统(内核)线程。
我们前面说1对1的模型的缺点是创建和销毁的开销影响性能,Java中引入了线程池的概念,线程池中的线程是可以复用的,这样就避免了线程的频繁创建和销毁,也就是达到了类似N对1的效果。
但不能像多对1那样,随时从a任务切换到b任务,必须要执行完a任务才能执行b任务,且当线程池中的线程数远大于CPU核心数时,内核线程的抢占式调度会导致线程上下文切换,这样会影响性能。
(二):虚拟线程
虚拟线程的思想是使用的的是N对M的线程模型,即N个虚拟线程对应M个系统(内核)线程。
这个模型的优点是可以更好的利用系统资源,避免了线程过多导致的系统资源的浪费,也避免了线程过少导致的线程过于繁忙。
最主要的是 虚拟线程 是 轻量级 的,创建和销毁的开销很小,而且不会出现系统内核级别的线程上下文切换,这样就避免了线程上下文切换的开销。
如下图所示,虚拟线程其实也是一个任务,是在的线程池中执行的,线程池内的线程是我们的 平台线程 ,每个平台线程有自己的虚拟线程队列,当虚拟线程队列中的虚拟线程执行完毕后,会去执行其他虚拟线程队列中的虚拟线程,这样就避免有的线程过忙,有的线程过闲的情况。
这个线程池有什么特殊
眼尖的你可能已经发现了,这个线程池和我们之前说的线程池有什么不一样,这个线程池是 ForkJoinPool,它是Java7中引入的一个新的线程池,它的特点是 工作窃取。
所以其实神秘的 虚拟线程 就是基于 ForkJoinPool 实现的,关于 ForkJoinPool 线程池我有一篇文章介绍过,有兴趣的可以看一下:【Java并发编程线程池】 ForkJoinPool 线程池是什么 怎么工作的 和传统的ThreadPoolExecutor比较。
源码求证
我们来看一下 VirtualThread 的源码,看一下它是怎么实现的。我们使用oracle官方文档推荐的方式来创建一个 虚拟线程 并启动。
oracle 官方文档使用方式 https://docs.oracle.com/en/java/javase/21/core/virtual-thread...
// 创建一个虚拟线程并启动
Thread.ofVirtual().start(() -> System.out.println("Hello"));
跟着源码查看,在执行VirtualThread的start方法的时候,会吧虚拟线程当作任务放到scheduler中执行,再跟进看下这个scheduler的初始化
根据源码我们可以看见,这个scheduler是一个ForkJoinPool线程池,主要是通过这个scheduler来执行虚拟线程的任务,实现我们JDK的新特性 虚拟线程。
ForkJoinPool从JDK7开始就有,就算公司用的JDK7/8,我也可以说我们也可以用虚拟线程啦!(哈哈,开个玩笑)。
总结
虚拟线程适合什么场景
- 适合短时间的任务,因为虚拟线程是轻量级的,创建和销毁的开销很小;
- 适合IO密集型的任务,因为虚拟线程不会出现系统内核级别的线程上下文切换,这样就避免了线程上下文切换的开销;
- 适合任务量大的场景,因为虚拟线程是轻量级的,可以更好的利用系统资源,避免了线程过多导致的系统资源的浪费,也避免了线程过少导致的线程过于繁忙。
测试用例
我们用sleep模拟100W个1秒的IO操作,看看虚拟线程和平台线程的性能对比。
public static void main(String[] args) throws InterruptedException {
// 使用虚拟线程池
ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
executor(virtualThreadExecutor, "虚拟线程池");
// 创建一个固定大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
executor(fixedThreadPool, "固定大小的线程池");
// 创建一个缓存线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
executor(cachedThreadPool, "缓存线程池");
// 创建一个单线程的线程池
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
executor(singleThreadPool, "单线程的线程池");
}
/**
* 使用不同的线程池执行任务
*/
private static void executor(ExecutorService executor, String poolName) throws InterruptedException {
long start = System.currentTimeMillis();
for (int i = 0; i < 1_000_000; i++) {
int finalI = i;
executor.submit(() -> {
try {
performSimulatedIOOperation(finalI);
} catch (Exception e) {
System.out.println("Exception occurred: " + e.getMessage());
}
});
}
executor.shutdown();
executor.awaitTermination(1, java.util.concurrent.TimeUnit.MINUTES);
long end = System.currentTimeMillis();
System.out.println("使用线程池" + poolName + "执行任务耗时:" + (end - start) + "ms");
}
/**
* 模拟一个I/O操作
*/
private static void performSimulatedIOOperation(int index) throws InterruptedException {
Thread.sleep(1000); // Simulating a one-second I/O operation
}
测试结果截图(抱歉结果太久,其他的线程池没执行玩就截图了,等不了了):
从测试结果可以看出,虚拟线程池的执行时间是最短的,这也验证了我们之前说的虚拟线程适合IO密集型的任务,适合任务量大的场景,适合需要高并发的场景。
从这个应用场景来看,虚拟线程可以用于高并发的场景,适合短时间的任务,适合IO密集型的任务,适合任务量大的场景。可以和netty做一个很好的结合,做一个高性能的网络应用。
为此我也写了一个简单的RPC框架,使用虚拟线程来实现高并发的场景,求求读者老爷们给个star✨??,地址在这里:JGZHAN/lrpc,后续会持续完善,欢迎大家一起交流学习。
好的,这就是我对Java中的虚拟线程 VirtualThread 的一些理解,希望对大家有所帮助,如果有疑问的地方,欢迎大家评论区留言讨论。
下期我会写一篇关于这个项目的文章,敬请期待,有兴趣的也可以和我一起来完善这个项目。
最后最后记得帮我的点个star✨,谢谢大家!
戳这里去点star
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。