Java虚拟线程
什么是虚拟线程?
虚拟线程是一种轻量级线程,旨在大大降低编写、维护和观察高吞吐量并发应用程序的工作量。它们最初作为JEP 425的预览功能提出,并在JDK 19中实现。为了获得更多反馈和经验,虚拟线程在JEP 436中再次作为预览功能推出,并在JDK 20中实现。该JEP提议在JDK21中最终确定虚拟线程
线程生命周期
以下是一个典型的数据库操作中,线程的生命周期:
-
线程在线程池中被创建
-
线程在池中等待新请求到来
-
线程接收请求并进行后端数据库调用以处理该请求
-
线程等待后端数据库的响应
-
线程处理数据库响应并将其发送给客户端
-
线程返回到线程池
步骤2到步骤6会重复,直到应用程序关闭。实际上,线程只在步骤3和步骤5中做实际工作。在所有其他步骤中,线程基本上是在等待。这意味着大多数应用程序中,线程在其生命周期的大部分时间是处于等待状态。
平台线程架构
在以前的JVM版本中,只有一种类型的线程,称为"经典"或"平台"线程。每当创建一个平台线程时,操作系统线程就会为其分配资源。只有当平台线程退出时,该操作系统线程才能被释放用于执行其他任务。在此之前,它无法执行任何其他任务。平台线程与操作系统线程之间存在一对一的映射关系。
这种架构的问题在于,即使线程在生命周期中的某些阶段没有做任何事情(如步骤1、2、4和6),操作系统线程也会被不必要地锁定。由于操作系统线程是宝贵且有限的资源,这种平台线程架构会导致大量资源浪费。
图示:每个平台线程对应一个操作系统线程
虚拟线程架构
为了更有效地利用底层操作系统线程,JDK 19引入了虚拟线程。在这种新的架构中,虚拟线程只有在执行实际工作的阶段才会分配给一个平台线程(也称为载体线程)。在其他阶段,虚拟线程将像应用程序的其他对象一样驻留在Java堆内存中。因此,它们更加轻量且高效。
图示:直到需要执行真正的工作时,操作系统线程才会分配给平台线程
图示:虚拟线程在执行真正的工作时才会映射到平台线程。
创建虚拟线程
所有与当前平台线程配合工作的API将不受影响,可以直接与虚拟线程一起使用。然而,用于创建Java虚拟线程的API略有不同。以下是一些创建Java虚拟线程的示例:
- Thread.startVirtualThread()
java
public static void main(String[] args) {
Runnable task = () -> {System.out.println("Hello Virtual Thread");};
Thread.startVirtualThread(task);
}
- Thread.ofVirtual().start()
java
public static void main(String[] args) {
Runnable task = () -> {System.out.println("Hello Virtual Thread");};
Thread.ofVirtual().start(task);
}
- Thread.ofVirtual().unstarted()
如果想要使用正确的属性(如名称、优先级等)创建线程对象,然后再启动线程,可以使用这种方法。
java
public static void main(String[] args) {
Runnable task = () -> {System.out.println("Hello Virtual Thread");};
hread vThread = Thread.ofVirtual().unstarted(task);
vThread.start();
}
- Executors.newVirtualThreadPerTaskExecutor()
Executor是一个强大的机制,用于在应用程序中创建、管理和关闭线程池。这个API为每个任务启动一个新的虚拟线程,在这种情况下,执行器创建的虚拟线程的数量是无限的。
java
public static void main(String[] args) {
Runnable task = () -> {System.out.println("Hello Virtual Thread");};
Executor vExecutor = Executors.newVirtualThreadPerTaskExecutor();
vExecutor.execute(task);
}
- Executors.newThreadPerTaskExecutor()
这个API是对前一个API的增强,可以将ThreadFactory作为输入参数传递,然后创建虚拟线程。在ThreadFactory中,可以设置虚拟线程的属性,如线程名称、线程优先级等。
java
public static void main(String[] args) {
Runnable task = () -> {System.out.println("Hello Virtual Thread");};
ThreadFactory vThreadFactory = Thread.ofVirtual().name("vt-", 1).factory();
Executor vExecutor = Executors.newThreadPerTaskExecutor(vThreadFactory);
vExecutor.execute(task);
}
虚拟线程的优点
减少应用程序内存消耗
在传统的平台线程架构中,平台线程和操作系统线程之间存在1:1的映射。在线程生命周期的第1步、第2步、第4步和第6步,操作系统线程将被不必要地锁定,即使在这些步骤中它并没有执行任何操作。尽管操作系统线程是宝贵且有限的资源,但它们在这种平台线程架构中的时间被大量浪费。
虚拟线程在等待时(即第1步、第2步、第4步、第6步),它们被存储在Java堆内存区域中,就像任何其他对象一样。与操作系统线程不同,这种堆栈块对象并不占用太多内存。只有当虚拟线程需要执行实际工作时,它才会绑定到底层操作系统上。因此,相比之下,虚拟线程占用的内存要少得多,远远少于平台线程。
提高应用程序的可用性
在大多数应用程序中,常见的问题是线程等待后端系统响应。当后端系统出现故障或者响应缓慢时,传统的线程池架构可能会导致应用无法处理新请求并变得不可用。然而,通过将应用服务器线程池配置为Java虚拟线程,我们可以创建数百万个轻量级线程。当线程因等待后端系统响应而被阻塞时,它们会以轻量级对象的形式存储在Java堆区域中。这种方法显著提高了应用程序的可用性,确保即使面对后端系统问题也能够持续处理新请求,从而避免应用程序变得不可用。
提高应用程序的吞吐量
在大多数架构中,应用程序可以处理的请求数量与应用服务器线程池中可用线程的数量成正比。传统的线程模型中,每个客户请求都需要一个独立的操作系统线程来处理,因此可用线程数量的限制直接影响了应用程序的并发处理能力和吞吐量。将应用服务器线程池配置为Java虚拟线程后,可以创建数百万个轻量级线程,从而显著提高应用程序的吞吐量。
此外,在某些应用程序中,可用线程数量先达到饱和,其次才是其他计算资源(如CPU、内存、网络、存储)。在这种情况下,使用虚拟线程对提升应用程序性能将会有显著的帮助。
减少"OutOfMemoryError:unabletocreatenewnativethread"错误
这种类型的OutOfMemoryError发生在两种情况下:
- 当应用程序创建的线程数量超过了服务器(或容器)的RAM容量。
- 当应用程序创建的线程数量超过了操作系统允许的限制(在操作系统中,存在一个内核限制,规定了单个进程可以创建的线程数量)。
虚拟线程很好地解决了这两个问题。虚拟线程比平台线程轻量得多,不会占用太多内存。因此,使用虚拟线程饱和RAM容量要比使用平台线程困难得多。虚拟线程不需要被分配给操作系统线程,除非它们需要执行实际工作。因此,与平台线程相比,虚拟线程应用程序产生超出操作系统线程限制的可能性要小得多。
提高代码质量
一些应用程序采用了反应式、异步编程模型以节省线程资源,这种模型能够高效利用线程。然而,这种编程模型也使代码变得更加复杂。在此模型中,应用程序发起后端调用时会启动独立的线程,该线程会立即返回而不等待后端系统的响应。一旦后端系统做出响应,会触发回调函数,但该函数由不同的线程执行,而非发起操作的线程执行。因此,代码需要被拆分成不同部分,而不是按顺序排列。这种方式增加了代码的维护性、可读性和调试难度。
与之相反,使用Java虚拟线程可以创建数百万个线程,并且按顺序执行整个后端调用代码,从而提高了代码的可读性和可维护性。由于线程不再是瓶颈,这种方式降低了对线程资源的紧缺问题。
与平台线程完全兼容
虚拟线程与平台线程的API完全兼容。虚拟线程支持平台线程的所有功能和API,比如:线程局部变量、同步块、线程中断等。只需要更改代码(或配置)来启动线程的方式(即,将平台线程替换为虚拟线程)。
参考资料
通过对Java虚拟线程的详细介绍和应用场景的分析,希望读者能够更好地理解并应用这一强大的并发编程工具。Java虚拟线程无疑为高性能并发应用程序的开发带来了新的可能性。