Hello folks,我是 Luga,今天我们来聊一下 Java 生态的核心技术—— Java Virtual Threads,即 “Java
虚拟线程” 。
虚拟线程是 Java 中的一个重要创新,在 Project Loom 项目中开发的。自从 Java 19 开始作为预览功能引入,到 Java 21
以后成为正式版本(JEP 444),虚拟线程已经成为 JDK 的一部分。
一、为什么是 Java Virtual Threads ?
众所周知,JVM 是一个多线程环境,通过 java.lang.Thread 类型为我们提供了对操作系统线程的抽象。在 Project Loom
之前,JVM 中的每个线程都只是对操作系统线程的一种简单封装,我们可以称之为“平台线程”。
然而,所谓的“平台线程”,在某些特定的业务场景中,往往存在一些问题,从多个角度来看,它们都是昂贵的。首先,创建平台线程的成本很高。每当创建一个平台线程时,操作系统必须在堆栈中分配大量内存(以兆字节计)来存储线程的上下文、原生调用堆栈和
Java 调用堆栈。由于堆栈大小是固定的,这就导致了高昂的内存开销。此外,每当调度器对线程进行抢占式调度时,也需要移动大量的内存。
因此,我们可以想象,这在空间和时间上都是非常昂贵的操作。实际上,由于堆栈框架的巨大尺寸限制,我们对可创建的线程数量也存在限制。在 Java
中,我们很容易遇到 OutOfMemoryError,只需不断实例化新的平台线程,直到操作系统的内存耗尽为止。
private static void stackOverFlowErrorExample() {
for (int i = 0; i {
try {
Thread.sleep(Duration.ofSeconds(1L));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
由于平台线程的创建成本较高,每个线程需要分配一定数量的堆栈内存,因此在某些情况下,如果我们不断实例化新的平台线程,直到操作系统的内存耗尽,就有可能迅速触发
OutOfMemoryError。
然而,这个过程的确切时间取决于多个因素,包括可用的内存大小、操作系统的线程限制以及 JVM 的配置。如果可用的内存较小,同时 JVM
的堆大小也较小,那么在不断实例化新的平台线程时,很可能会很快达到内存的极限,导致 OutOfMemoryError 的发生。
[0.949s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 4k, detached.
[0.949s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-4073"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
上述示例展示了我们如何基于当前的受到限制的环境中进行并发编程。
然而,Java 自从问世以来一直致力于成为一种简单易用的编程语言。在并发编程领域,我们应该像编写顺序代码一样编写程序。事实上,在 Java
中,为每个并发任务创建一个新线程是编写并发程序更简单的方法之一。这种模型被称为"每个线程一个任务"。
接下来,我们来看一下虚拟线程内部架构,具体如下所示:
使用这种方法,每个线程可以使用自己的局部变量来存储信息,从而大大减少了共享可变状态的需求。线程之间共享状态是并发编程中众所周知的"棘手部分"。然而,通过每个线程一个任务的模型,我们可以轻松地避免复杂的线程同步和共享状态的问题。
然而,正如之前提到的,使用这种方法也存在着限制,即我们能够创建的线程数量有限。由于平台线程的创建成本较高,每个线程都需要分配一定数量的堆栈内存,这限制了我们可以创建的线程数量。如果我们不加限制地创建大量线程,就有可能导致内存耗尽和性能下降。
需要注意的是,随着 Project Loom
的引入,虚拟线程的轻量级特性将显著改善线程创建成本和内存开销。这将使我们能够更轻松地创建大规模的并发任务,而不会受到线程数量限制的困扰。
二、如何创建 Virtual Threads ?
正如我们之前所提到的,虚拟线程是一种新型的线程,旨在解决平台线程的资源限制问题。它们是 java.lang.Thread 类型的替代实现,将堆帧(Heap
Frame)存储在堆内存中,而不是堆栈中。
由于虚拟线程的堆栈存储在堆中,因此它们的初始内存占用非常小,通常只有几百字节,而不是兆字节。此外,堆栈块的大小可以动态调整。这意味着我们不需要为每个可能的用例分配数百兆字节的内存。
通常而言,创建一个新的虚拟线程非常简单。我们可以使用 java.lang.Thread类 型上的新工厂方法 ofVirtual
来实现。让我们首先定义一个实用函数,用于创建具有给定名称的虚拟线程的示例代码:
import java.lang.Thread;
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.ofVirtual("VirtualThreadExample", VirtualThreadExample::runTask);
virtualThread.start();
}
public static void runTask() {
// 在虚拟线程中执行的任务代码
System.out.println("Running task in virtual thread");
}
}
在上面的示例中,我们使用 Thread.ofVirtual 方法创建了一个名为 "VirtualThreadExample"
的虚拟线程,并指定了要在其中执行的任务代码。然后,我们调用 start 方法启动虚拟线程。
通过使用虚拟线程,我们可以更加灵活地管理线程的内存消耗,并提高并发程序的性能和可伸缩性。虚拟线程是 Project Loom 的关键特性之一,将极大地改善
Java 中的并发编程体验。
三、Virtual Threads 到底有哪些方面优势?
作为 Project Loom
提出的一种新的线程模型,即虚拟线程。虚拟线程是一种轻量级的线程,其堆栈存储在堆内存中,而不是在操作系统线程的堆栈中。这种设计使得虚拟线程的创建和销毁成本较低,并且可以创建大量的线程,而不会受到操作系统和硬件资源的限制。
虚拟线程的引入将改变 Java
中的并发编程方式。它们可以通过更高效地利用系统资源来提高并发性能,并且可以简化并发编程的复杂性。虚拟线程可以使用更少的内存,并且可以根据需求动态调整堆栈的大小,以提高资源利用率。
具体可参考如下所示:
1、减少应用程序内存消耗
与传统的由平台线程都映射到操作系统线程的生命周期相对比,虚拟线程通过较小的初始内存占用、动态调整堆栈大小、共享堆栈和更高效的内存管理等方式,减少了应用程序的内存消耗。这使得可以创建更多的线程,提高并发性能,并且更有效地利用系统资源。
2、提高应用程序吞吐量
在大多数架构中,应用程序可以处理的请求数量与应用程序服务器线程池中可用的线程数量成正比。因为每个客户请求都由单个唯一的线程处理。因此,如果可用的线程数量较少,则只能同时处理少量请求。这将降低应用程序的吞吐量。另一方面,如果应用程序服务器线程池配置了Java虚拟线程,它可以创建明显更高的线程数量(数百万),这将最终提高应用程序的吞吐量。
此外,在某些应用程序中,应用程序服务器线程池中的可用线程在其他计算资源(如CPU、内存、网络、存储)饱和之前首先饱和。对于这样的虚拟线程来说,这将是一个较大的增强。
3、减少无法创建新的本机线程的 “OutOfMemoryError” 异常
在 JVM
上运行的应用程序容易出现“java.lang.OutOfMemoryError:无法创建新的本机线程”。这种类型的内存错误通常发生在如下两种情况下:
(1)当应用程序创建的线程超过服务器(或容器)的 RAM 容量时
(2)当应用程序创建的线程超过操作系统允许的限制时(注:在操作系统中,有一个内核限制,该限制规定了单个进程可以创建的线程数量)。
通常而言,Java 虚拟线程在减少内存消耗方面具有显著优势。相比传统的平台线程,Java
虚拟线程通常更轻量级,它们占用的内存较少。这使得使用虚拟线程比使用平台线程更难达到 RAM 容量的饱和。
传统的平台线程需要分配操作系统线程,并且每个线程都有一定的内存开销。而虚拟线程在不做实际工作时,并不需要分配操作系统线程,因此虚拟线程应用程序超过操作系统线程限制的可能性要远远高于传统的平台线程。
虚拟线程的轻量级特性和更高的灵活性使得可以创建更多的线程,而不会受到操作系统和硬件资源的限制。这进一步增加了虚拟线程应用程序处理大规模并发的能力,提高了系统的可伸缩性。
4、提高应用程序可用性
在我们主流的系统架构中,应用程序通常需要与多个后端系统进行通信,如
API、数据库和第三方框架等。然而,当其中一个后端系统出现中断或响应缓慢时,传统的应用程序服务器线程会被阻塞,等待后端系统的响应。随着更多请求进入应用程序,越来越多的线程会被阻塞。在这种情况下,应用程序服务器线程池中的线程数量是有限的。如果所有线程都被阻塞等待后端系统的响应,那么就没有可用线程来处理新的请求,从而导致整个应用程序不可用。
然而,通过将应用程序服务器线程池配置为使用 Java
虚拟线程,可以解决上述问题并提高应用程序的可用性。使用虚拟线程,我们甚至可以轻松创建数百万个线程,而不会出现重大问题。当虚拟线程被阻塞等待后端系统的响应时,它会像任何其他应用程序对象一样,以非常轻量级的方式存储在
Java 堆区域中。因此,应用程序服务器线程池可以继续创建虚拟线程,而不会耗尽线程池中的线程资源,直到后端系统恢复。
这种优化策略为应用程序带来了巨大的潜力,提高了应用程序的可用性。即使在后端系统出现问题时,应用程序仍然能够继续创建和处理请求,而不会因为线程资源的耗尽而导致不可用状态。这种灵活性和弹性使得应用程序能够更好地应对高负载和故障情况,保持稳定的运行状态。
Java 虚拟线程提供了现代应用程序所需的强大且高效的并发模型。它简化了并发编程,并带来更好的资源利用率,因此有可能彻底改变开发人员在 Java
中处理并发代码的方式。
随着 Java 技术不断发展和创新,了解最新的功能如虚拟线程对于那些希望保持领先地位并充分利用 Java 生态系统潜力的开发人员来说至关重要。
虚拟线程提供了一种轻量级的线程模型,通过协作调度和高效的内存管理,大大减少了线程创建和管理的开销。这使得开发人员能够更容易地编写高性能、高并发的应用程序,而无需担心传统线程模型的限制和开销。
通过使用虚拟线程,开发人员可以更好地利用系统资源,提高应用程序的并发性能。虚拟线程的出现为 Java
生态系统带来了更多的潜力和机会,使得开发人员能够更好地应对现代应用程序中的并发需求。
因此,对于那些希望保持领先并充分利用 Java
生态系统的开发人员来说,了解虚拟线程等先进功能是至关重要的。这将使他们能够更好地应对并发编程挑战,并构建出高性能、可扩展的应用程序,从而在竞争激烈的软件开发市场中脱颖而出。