一文了解Java中的虚拟线程新特性
部分内容来源:JavaGuide
简单说一下为什么要有虚拟线程
为什么我们要有虚拟线程
我们创建线程和销毁线程是有开销的,我们的线程池只是线程的复用,用作一个辅助,减少了创建和销毁线程的开销,但并不会提高我们的系统可以合理使用线程数上限,因为我们机器的线程数量上限是由我们的操作系统决定的
所以为了提高并发量,我们采用异步编程的方式来解决这个问题,也就是采用我们的非阻塞模型
再异步编程的代码中充满了大量的回调函数,它学习成本高同时不利于调试和代码的维护
就是在这个背景下,我们的虚拟线程出现了,它用于提高我们的阻塞IO的并发量
什么是虚拟线程
虚拟线程是以平台线程为载体的轻量级线程
它运行在用户空间内由JVM进行管理和调度,它占用的资源更少同时可以减少线程切换的开销
举个简单的例子,我们插座优先,我们想要插更多的插头那么我们就要新装多个插座,这样子会很麻烦,但是虚拟线程就好比我们的插排,我们在插座上多加几个插排就可以提高我们电器的使用数量
虚拟线程和平台线程的对应关系
一个平台线程可以对应多个虚拟线程
每一个平台线程都用一个任务列表
平台线程从任务列表里面去获取虚拟线程
当虚拟线程启动的时候,他会被绑定到一个平台线程
平台线程作为载体,它是位于一个线程池中的,这个线程池就是Fork Join Pool
当某个虚拟线程被阻塞(比如等待 I/O 操作完成、等待锁等情况)或者处于等待状态时,平台线程并不会一直等待它恢复,而是可以切换去执行另一个虚拟线程。这样可以提高线程资源的利用率,因为在传统的平台线程中,当线程阻塞时,对应的内核线程也会被阻塞,而虚拟线程可以让平台线程在虚拟线程阻塞时去做其他有意义的工作
也就是平台线程可以去运行其他的虚拟线程的任务,这个行为被称为偷
当虚拟线程从阻塞状态恢复正常后,那么它会被加入到某个平台线程的任务列表中,然后再次被绑定到了某个平台线程上
虚拟线程和平台线程并不是一 一绑定的,他可能会在Blocked之后被重新调度到其他线程上
这样子我们就可以使用少量的平台线程资源去运行大量的虚拟线程
适用场景
- 虚拟线程适用于执行阻塞式任务 例如IO读写,在阻塞期间,可以将 CPU 资源让渡给其他任务。
- 虚拟线程不适合 CPU 密集计算或非阻塞任务,虚拟线程并不会运行得更快,而是增加了规模。
- 虚拟线程是轻量级资源,用完即抛,不需要池化。
- 通常我们不需要直接使用虚拟线程,像 Tomcat、Jetty、Netty、Spring boot 等都已支持虚拟线程
为什么我们要有虚拟线程
我们创建线程和销毁线程是有开销的,我们的线程池只是线程的复用,用作一个辅助,减少了创建和销毁线程的开销,但并不会提高我们的系统可以合理使用线程数上限,因为我们机器的线程数量上限是由我们的操作系统决定的
所以为了提高并发量,我们采用异步编程的方式来解决这个问题,也就是采用我们的非阻塞模型
再异步编程的代码中充满了大量的回调函数,它学习成本高同时不利于调试和代码的维护
就是在这个背景下,我们的虚拟线程出现了,它用于提高我们的阻塞IO的并发量
什么是虚拟线程
虚拟线程是以平台线程为载体的轻量级线程
它运行在用户空间内由JVM进行管理和调度,它占用的资源更少同时可以减少线程切换的开销
举个简单的例子,我们插座优先,我们想要插更多的插头那么我们就要新装多个插座,这样子会很麻烦,但是虚拟线程就好比我们的插排,我们在插座上多加几个插排就可以提高我们电器的使用数量
虚拟线程和平台线程的对应关系
一个平台线程可以对应多个虚拟线程
每一个平台线程都用一个任务列表
平台线程从任务列表里面去获取虚拟线程
当虚拟线程启动的时候,他会被绑定到一个平台线程
平台线程作为载体,它是位于一个线程池中的,这个线程池就是Fork Join Pool
当某个虚拟线程被阻塞(比如等待 I/O 操作完成、等待锁等情况)或者处于等待状态时,平台线程并不会一直等待它恢复,而是可以切换去执行另一个虚拟线程。这样可以提高线程资源的利用率,因为在传统的平台线程中,当线程阻塞时,对应的内核线程也会被阻塞,而虚拟线程可以让平台线程在虚拟线程阻塞时去做其他有意义的工作
也就是平台线程可以去运行其他的虚拟线程的任务,这个行为被称为偷
当虚拟线程从阻塞状态恢复正常后,那么它会被加入到某个平台线程的任务列表中,然后再次被绑定到了某个平台线程上
虚拟线程和平台线程并不是一 一绑定的,他可能会在Blocked之后被重新调度到其他线程上
这样子我们就可以使用少量的平台线程资源去运行大量的虚拟线程
适用场景
- 虚拟线程适用于执行阻塞式任务 例如IO读写,在阻塞期间,可以将 CPU 资源让渡给其他任务。
- 虚拟线程不适合 CPU 密集计算或非阻塞任务,虚拟线程并不会运行得更快,而是增加了规模。
- 虚拟线程是轻量级资源,用完即抛,不需要池化。
- 通常我们不需要直接使用虚拟线程,像 Tomcat、Jetty、Netty、Spring boot 等都已支持虚拟线程
什么是虚拟线程
虚拟线程(Virtual Thread)是 JDK 而不是 OS 实现的轻量级线程(Lightweight Process,LWP),由 JVM 调度
许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量
虚拟线程和平台线程有什么关系
在引入虚拟线程之前,java.lang.Thread
包已经支持所谓的平台线程(Platform Thread),也就是没有虚拟线程之前,我们一直使用的线程
JVM 调度程序通过平台线程(载体线程)来管理虚拟线程,一个平台线程可以在不同的时间执行不同的虚拟线程(多个虚拟线程挂载在一个平台线程上)
当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程
虚拟线程、平台线程和系统内核线程的关系图如下所示(图源:How to Use Java 19 Virtual Threads):
关于平台线程和系统内核线程的对应关系多提一点:在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个平台线程对应一个系统内核线程
Solaris 系统是一个特例,HotSpot VM 在 Solaris 上支持多对多和一对一
假设有一个平台线程(载体线程)和两个虚拟线程(VT1
、VT2
):
- VT1 运行:载体线程执行
VT1
的代码。 - VT1 阻塞(如等待I/O):JVM检测到阻塞,立即挂起
VT1
,保存其栈状态到堆内存。 - VT2 运行:载体线程切换到执行
VT2
的代码,无需等待操作系统切换线程。 - VT1 就绪(如I/O完成):JVM可能将
VT1
重新调度到任意可用载体线程(不一定是原来的)继续执行。
整个过程平台线程始终在忙碌,没有被阻塞,只是通过快速切换虚拟线程来最大化利用率
为何说“看似让给虚拟线程”?
代码编写视角:开发者可以像使用普通线程一样创建海量虚拟线程(如每个请求一个虚拟线程),而无需关心底层平台线程的数量限制。
// 传统方式(平台线程):无法承受百万线程
ExecutorService executor = Executors.newFixedThreadPool(200);// 虚拟线程方式:轻松创建百万“线程”
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
性能提升:虚拟线程通过避免平台线程的阻塞和频繁切换,使得同样的硬件资源可以处理更高的并发量,看似“接管”了任务调度
虚拟线程有什么优点和缺点
优点
非常轻量级:可以在单个线程中创建成百上千个虚拟线程而不会导致过多的线程创建和上下文切换,用完即可丢弃,无需像传统线程那样进行复杂的池化管理,减少资源开销
简化异步编程: 虚拟线程可以简化异步编程,使代码更易于理解和维护。它可以将异步代码编写得更像同步代码,避免了回调地狱(Callback Hell)。
减少资源开销: 由于虚拟线程是由 JVM 实现的,它能够更高效地利用底层资源,例如 CPU 和内存 虚拟线程的上下文切换比平台线程更轻量,因此能够更好地支持高并发场景
缺点
不适用于计算密集型任务: 当虚拟线程被阻塞或等待时,平台线程可以切换到执行另一个虚拟线程 虚拟线程适用于 I/O 密集型任务,但不适用于计算密集型任务,因为密集型计算始终需要 CPU 资源作为支持。
与某些第三方库不兼容: 虽然虚拟线程设计时考虑了与现有代码的兼容性,但某些依赖平台线程特性的第三方库可能不完全兼容虚拟线程
如何创建虚拟线程
官方提供了以下四种方式创建虚拟线程:
- 使用
Thread.startVirtualThread()
创建 - 使用
Thread.ofVirtual()
创建 - 使用
ThreadFactory
创建 - 使用
Executors.newVirtualThreadPerTaskExecutor()
创建
1、使用 Thread.startVirtualThread()
创建
public class VirtualThreadTest {public static void main(String[] args) {CustomThread customThread = new CustomThread();Thread.startVirtualThread(customThread);}
}static class CustomThread implements Runnable {@Overridepublic void run() {System.out.println("CustomThread run");}
}
2、使用 Thread.ofVirtual()
创建
public class VirtualThreadTest {public static void main(String[] args) {CustomThread customThread = new CustomThread();// 创建不启动Thread unStarted = Thread.ofVirtual().unstarted(customThread);unStarted.start();// 创建直接启动Thread.ofVirtual().start(customThread);}
}
static class CustomThread implements Runnable {@Overridepublic void run() {System.out.println("CustomThread run");}
}
3、使用 ThreadFactory
创建
public class VirtualThreadTest {public static void main(String[] args) {CustomThread customThread = new CustomThread();ThreadFactory factory = Thread.ofVirtual().factory();Thread thread = factory.newThread(customThread);thread.start();}
}static class CustomThread implements Runnable {@Overridepublic void run() {System.out.println("CustomThread run");}
}
4、使用Executors.newVirtualThreadPerTaskExecutor()
创建
public class VirtualThreadTest {public static void main(String[] args) {CustomThread customThread = new CustomThread();ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();executor.submit(customThread);}
}
static class CustomThread implements Runnable {@Overridepublic void run() {System.out.println("CustomThread run");}
}
虚拟线程适用的场景是啥
在密集 IO 的场景,虚拟线程可以大幅提高线程的执行效率,减少线程资源的创建以及上下文切换
虚拟线程关键总结
平台线程仍是基石:所有代码最终仍需平台线程执行,虚拟线程只是优化了它们的用法
虚拟线程是“用户态调度”:JVM在用户空间(而非操作系统内核)实现了轻量级线程调度,避免陷入内核的高成本
资源利用率提升:虚拟线程让平台线程始终处理有效工作(而不是等待),从而将硬件资源(如CPU、内存)的潜力发挥到极致