探索虚拟线程:原理与实现
虚拟线程的引入与优势
在 Loom 项目之前,Java 虚拟机(JVM)中的线程是通过java.lang.Thread
类型来实现的,这些线程被称为平台线程。
然而,平台线程的创建和维护在资源使用上存在显著的开销。首先,创建成本不菲,因为每当操作系统需要创建一个新的平台线程时,它必须分配大量的内存(通常以兆字节计)来存储线程的上下文信息、本机栈和 Java 调用栈。这一过程受到固定大小堆栈的限制,导致创建和调度平台线程时的开销在空间和时间上都相当巨大。此外,当调度器需要从当前执行的线程中抢占时,必须处理大量内存的移动,这进一步增加了操作的复杂性和成本。这种开销不仅限制了可以同时创建的线程数量,而且也容易导致内存资源的耗尽。以下是一个示例,展示了在 Java 中如何通过不断实例化新的平台线程,迅速达到内存耗尽的情况:
在实际操作中,达到OutOfMemoryError
的时间会根据操作系统和硬件的不同而有所差异。然而,通常情况下,这个过程可以在极短的时间内完成。
为了解决这些问题,虚拟线程应运而生。
虚拟线程的优势
资源效率:虚拟线程在内存使用上更为高效,初始内存占用通常只有几百字节,远小于平台线程所需的几兆字节。
简化线程管理:虚拟线程的创建和管理过程更为简便,通过工厂方法可以轻松创建,无需手动管理线程资源。
避免线程爆炸:由于资源消耗低,虚拟线程可以处理大量并发任务,而不必担心资源耗尽。
协作调度:虚拟线程采用协作调度模型,减少了锁竞争和上下文切换的开销,提升了多线程程序的性能。
避免阻塞:虚拟线程在遇到阻塞操作时可以释放执行权,允许其他线程执行,提高了程序的响应性。
虚拟线程如何创建
创建虚拟线程是 Java 中的一项新特性,它旨在解决传统平台线程所面临的资源限制问题。虚拟线程作为java.lang.Thread
的一个替代实现,其独特之处在于将线程的调用堆栈存储在 Java 堆内存中,而不是传统的本地线程堆栈中。这种方式显著减少了每个线程所需的初始内存占用,通常仅为几百字节,而不是几兆字节。更进一步,虚拟线程的堆栈大小是动态可变的,这使得我们无需为各种用例预分配大量内存。以下是创建虚拟线程的两种方法:
使用工厂方法创建虚拟线程
通过java.lang.Thread
的ofVirtual
静态工厂方法,我们可以轻松创建虚拟线程。首先,定义一个辅助函数来创建并启动一个带有指定名称的虚拟线程:
使用 ThreadPerTaskExecutor 创建虚拟线程
另一种方法是使用专为虚拟线程设计的java.util.concurrent.ExecutorService
实现,即ThreadPerTaskExecutor
。这个执行器为提交的每个任务创建一个新的虚拟线程:
在这个示例中,我们使用了submit
方法来启动虚拟线程,它需要一个Runnable
或Callable
任务。submit
方法返回一个Future
对象,该对象可以用来跟踪和控制虚拟线程的执行。
虚拟线程的启动和同步
与平台线程相比,虚拟线程的启动和同步方式略有不同,因为它们是通过ExecutorService
来管理的。每个submit
调用都返回一个Future
对象,这允许我们跟踪任务的状态,甚至在必要时阻塞当前线程直到虚拟线程完成其任务。
虚拟线程的原理
如上图所示展示虚拟线程与平台线程之间的关系:
JVM 维护了一个由专用 ForkJoinPool 创建和维护的平台线程池。最初,平台线程的数量等于 CPU 核心的数量,最多不能超过 256 个。
对于每个创建的虚拟线程,JVM 都会将其执行调度到一个平台线程上,临时将虚拟线程的堆栈块从堆复制到平台线程的堆栈中。我们说平台线程变成了虚拟线程的载体线程。
我们可以通过运行使用ThreadPerTaskExecutor
创建虚拟线程的用例,观察其中的一条日志来说明执行过程:
从日志中进行观察
线程标识与命名:每个虚拟线程都有一个唯一的标识符和名称,例如
VirtualThread[#23,worker-1]
。这里的#23
表示线程的编号,而worker-1
是线程的名称,它们共同帮助开发者识别和调试线程。载体线程的分配:虚拟线程执行时,会绑定到一个特定的载体线程(即平台线程)。例如,
ForkJoinPool-1-worker-2
表示该虚拟线程正在由默认的 ForkJoinPool 中的第二个工作线程执行。阻塞与释放:当虚拟线程遇到阻塞操作时,其载体线程会被释放,以便能够执行其他就绪的虚拟线程。同时,虚拟线程的堆栈块会从载体线程的堆栈复制回 Java 堆中,以等待阻塞操作的完成。
再次调度:一旦虚拟线程完成其阻塞操作,调度器会将其重新排入执行队列。虚拟线程可能会继续在先前的载体线程上执行,或者根据调度器的决策,在不同的载体线程上继续执行。
刚才我们提到,默认情况下,JVM 会创建与 cpu 核心数量相等的载体线程(平台线程),以确保每个物理核心都能被有效利用。那么假如计算机上配备了 2 个物理核心和通过超线程技术支持的 4 个逻辑核心,基于此硬件配置,我们可以设计一个程序,该程序旨在生成与逻辑核心数相匹配的虚拟线程数量,即 4 个虚拟线程。然而,为了探索线程调度的灵活性,我们可以增加一个额外的虚拟线程,使得总数达到 5 个,即期望 5 个虚拟线程在 4 个载体线程上执行,那么至少会有一个载体线程会被重复使用。执行以下程序
观察日志,有四个载体线程,分别是 ForkJoinPool-1-worker-1、ForkJoinPool-1-worker-2、ForkJoinPool-1-worker-3 和 ForkJoinPool-1-worker-4,ForkJoinPool-1-worker-4 被重复使用了两次,以上假设正确。
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/de94a27c1e3c6ad8355500773】。文章转载请联系作者。
评论