Java19 虚拟线程原理介绍及实现

本文介绍Java19引入的虚拟线程特性,探讨其使用方法及内部机制,并与Go协程进行对比,帮助读者理解虚拟线程如何提高并发性能。

Java19发布,带来了 Java 开发者期待已久的新特性——虚拟线程。在 Java 有这个新特性之前,Golang 的 协程已经流行了很长时间,在并发编程领域大获成功。随着 Golang 的快速发展和推广,协程似乎已经成为世界上最好的语言的必备特性之一。

 Java19 虚拟线程可以填补这一空白。在这篇文章中,我们将带你通过对虚拟线程的介绍以及与 Golang  协程的对比,带你领略 Java19 虚拟线程的风采。

一、Java 19线程模型引入虚拟线程概念

java线程与虚拟线程

我们常见的Java线程与系统内核线程是一一对应的,系统内核线程调度器负责调度Java线程。为了提高应用程序的性能,我们会创建越来越多的Java线程,显然系统在调度Java线程时会消耗大量资源,来处理线程上下文切换。

 近几十年来,我们一直依靠上述多线程模型来解决 Java 中的并发编程问题。为了提高系统的吞吐量,我们必须不断增加线程的数量,但是机器的线程很昂贵,可用线程的数量是有限的。尽管我们使用各种线程池来最大限度地提高线程的成本效益,但在 CPU、网络或内存资源被耗尽之前,线程往往成为我们应用程序性能的瓶颈,无法释放硬件应具有的最大性能。

为了解决这个问题,Java19 引入了虚拟线程。在 Java19 中,我们以前使用的线程称为平台线程,仍然与系统内核线程一一对应。大量 (M个) 的虚拟线程,运行在少量 (N个) 的平台线程上(与 OS 线程一一对应)(M:N 调度)。JVM调度多个虚拟线程在特定平台线程上执行,并且在平台线程上一次只执行一个虚拟线程。

如何创建 Java 虚拟线程?

新的线程相关 API

Thread.ofVirtual()Thread.ofPlatform()分别用于创建虚拟和平台线程的新 API。

1
2
3
4
5
6
7
8

//output thread ID including virtual threads and system threads Thread.getId() deprecated from jdk19
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
// Create virtual threads
Thread thread = Thread.ofVirtual().name("testVT").unstarted(runnable);
testVT.start();
// Create virtual platform threads
Thread testPT = Thread.ofPlatform().name("testPT").unstarted(runnable);
testPT.start();

使用Thread.startVirtualThread(Runnable)快速创建虚拟线程并启动它。

1
2
3

// Output thread IDs including virtual threads and system threads
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread thread = Thread.startVirtualThread(runnable);

使用Thread.isVirtual()方法定义一个线程是否为虚拟线程的。

1
2

Runnable runnable = () -> System.out.println(Thread.currentThread().isVirtual());
Thread thread = Thread.startVirtualThread(runnable);

使用Thread.join()等待虚拟线程完成作业调度,使用Thread.sleep()讲虚拟线程致为休眠状态。

1
2
3
4

Runnable runnable = () -> System.out.println(Thread.sleep(10));
Thread thread = Thread.startVirtualThread(runnable);
// Wait for the virtual thread to finish
thread.join();

使用 Executors.newVirtualThreadPerTaskExecutor()。创建ExecutorService 。ExecutorService 可以为每个任务创建一个新的虚拟线程。

1
2
3

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
  executor.submit(() -> System.out.println("hello"));
}

也可以使用线程池和 ExecutorService ,完成现有代码的替换和迁移。

因为虚拟线程是Java19中的一个预览特性,所以本文出现的代码用到的运行命令如下

  • 使用 编译程序javac --release 19 --enable-preview Main.java并使用java --enable-preview Main.
  • 或者使用java --source 19 --enable-preview Main.java.

二、Java虚拟线程 VS Go 协程

实现原理对比

GO GMP模型

Go 语言采用两级线程模型,其中 Goroutine 是 M:N 与系统内核线程,符合 Java 虚拟线程。 最终的 goroutine 仍然交给 OS 线程执行,但需要一个中介来提供上下文。 这是 G-M-P 模型。

  • G:goroutine,类似于进程控制块,持有栈、状态、id、函数等。G只有绑定到P才能被调度。
  • M:机器,系统线程,绑定到一个有效的 P 然后被调度。
  • P:逻辑处理器,持有各种队列G。对于G来说,P是cpu核心。对于 M,P 是上下文。
  • sched:调度器,保存GRQ(全局运行队列)、M空闲队列、P空闲队列、锁等信息。

 排队

Go 调度程序有两个不同的运行队列。

  • GRQ,全局运行队列,尚未分配给 G for P(在 Go 1.1 之前只有 GRO 全局运行队列,但由于全局队列锁定的性能问题,添加了 LRQ 以减少锁定等待)。
  • LRQ,本地运行队列,每个P都有一个LRQ,负责管理分配给P的G。当LRQ中没有G要执行时,从GRQ中取出。

交接机制

当G执行阻塞操作时,GMP调度空闲M执行阻塞M LRQ中的其他G,以防止阻塞M影响LRQ中其他G的执行。

  • G1 在 M1 上运行,P 的 LRQ 有 3 个其他 G。
  • G1 进行同步调用,阻塞 M。
  • 当 M1 下只有 G1 运行且没有 P 时,Scheduler 将 M1 与 P 分离。
  • 将P绑定到空闲的M2,M2从LRQ中选择其他G运行。
  • G1 结束阻塞操作并返回到 LRQ。M1 被放置在空闲队列中进行备份。

偷工减料机制

GMP 为了最大化硬件的性能,任务窃取机制用于在 M 空闲时执行其他等待的 G。

  • 有两个P,P1和P2。
  • 如果 P1 的 Gs 都被执行,并且 LRQ 为空,则 P1 开始任务窃取。
  • 在第一种情况下,P1 从 GRQ 获得 G。
  • 在第二种情况下,P1 没有从 GRQ 获得 G,然后 P1 从 P2 LRQ 窃取 G。

切换机制是为了防止 M 阻塞,任务窃取是为了防止 M 空闲。

Java 虚拟线程调度模型

JDK 依赖于操作系统中的线程调度器来调度基于操作系统线程实现的平台线程。对于虚拟线程,JDK 有自己的调度程序。JDK的调度器不是直接将虚拟线程分配给系统线程,而是将虚拟线程分配给平台线程(这就是前面提到的虚拟线程的M:N调度)。平台线程由操作系统的线程调度系统调度。

JDK 的虚拟线程调度器是一个类似ForkJoinPoolFIFO 模式的线程池。调度程序中的并行量取决于调度程序虚拟线程中的平台线程数。默认值是可用的 CPU 内核数,但可以使用系统属性进行调整jdk.virtualThreadScheduler.parallelism。注意ForkJoinPool这里与 不同ForkJoinPool.commonPool(),后者用于实现并行流,以 LIFO 模式运行。

ForkJoinPoolExecutorService以不同的方式工作。ExecutorService有一个等待队列来存储其任务,其中的线程将接收并处理这些任务。虽然ForkJoinPool每个线程都有一个等待队列,但当一个线程运行的任务生成另一个任务时,该任务会添加到该线程的等待队列中,当我们运行Parallel Stream并将一个大任务分为两个较小的任务时会发生这种情况。

为了防止线程饥饿问题,当一个线程的等待队列中没有更多任务时,ForkJoinPool还实现了另一种称为任务窃取的模式,这意味着一个饥饿的线程可以从另一个线程的等待队列中窃取一些任务。这类似于 Go GMP 模型中的工作窃取机制。

虚拟线程执行 

通常,当虚拟线程在 JDK 中执行 I/O 或其他阻塞操作时,会从平台线程中卸载虚拟线程,例如BlockingQueue.take(). 当阻塞操作准备好完成时(例如,网络 IO 已接收到字节数据),调度程序将虚拟线程挂载到平台线程上以恢复执行。

JDK 中的大多数阻塞操作从平台线程中卸载虚拟线程,从而允许平台线程执行其他工作任务。但是,JDK 中的一些阻塞操作不会卸载虚拟线程,因此会阻塞平台线程。这是因为操作系统级别(例如,许多文件系统操作)或 JDK 级别(例如Object.wait())的限制。当这些阻塞操作阻塞平台线程时,它们会通过临时增加平台线程的数量来补偿其他平台线程阻塞的损失。因此,调度程序中的平台线程数ForkJoinPool可能会

<think>嗯,用户想了解Java虚拟线程实现原理。我需要先回忆一下Java 19中引入的虚拟线程,以及Project Loom的相关信息。首先,虚拟线程是为了解决传统线程的局限性,比如高并发时的资源消耗问题。传统线程每个都对应一个操作系统线程,创建数量有限,而虚拟线程更轻量,可以大量创建。 接下来,我得解释虚拟线程实现原理。可能涉及到JVM层面的调度,虚拟线程JVM管理,而不是直接依赖OS线程。需要提到它们如何被挂起和恢复,以及载体线程(carrier thread)的作用。还有,虚拟线程如何避免阻塞操作系统线程,通过状态保存和continuation。 用户可能对协程或者其他语言的类似实现(如Go的goroutine)有了解,可以比较一下。但要注意区分Java实现细节,比如虚拟线程是基于ForkJoinPool的调度器,任务窃取机制等。 另外,要提到虚拟线程的使用场景,比如高并发IO操作,如何提升吞吐量。可能还需要指出与传统线程模型的不同,比如无需改变现有代码结构,使用ExecutorService或新的结构化并发API。 需要确保术语准确,比如平台线程(以前的OS线程)、虚拟线程、载体线程这些概念要解释清楚。同时,注意避免过于深入代码细节,保持解释在原理层面,适合不同层次的读者理解。 最后,检查是否有遗漏的关键点,比如性能优势、资源利用率提升,以及虚拟线程如何与现有Java并发库兼容。可能还要提到调试和监控上的挑战,因为虚拟线程数量庞大,传统工具可能需要调整。Java</think> 虚拟线程(Virtual Threads)是 Java 19 引入的轻量级线程(JEP 425),目标是简化高并发编程,提升吞吐量并降低资源消耗。其核心原理是通过 **用户态调度** 和 **堆栈管理** 实现轻量化,而非依赖操作系统线程(称为 "平台线程")。以下是实现原理的关键点: --- ### 1. **与平台线程的对比** - **传统线程(平台线程)**:1:1 绑定操作系统线程,创建和切换成本高,数量受限于操作系统。 - **虚拟线程**:M:N 映射到平台线程(由 JVM 调度),创建和切换成本极低,可轻松创建数百万个。 --- ### 2. **核心机制** - **用户态调度**:虚拟线程的生命周期由 JVM 管理,而非操作系统。JVM 使用少量平台线程作为“载体线程”(Carrier Threads),负责执行虚拟线程的任务。 - **挂起与恢复**:当虚拟线程遇到阻塞操作(如 I/O、锁等),JVM 自动将其挂起,保存当前栈状态(Continuation),并释放载体线程去执行其他虚拟线程。 - **Continuation 机制**:通过保存和恢复线程的栈帧状态,实现非阻塞切换。这是通过 JVM 内部的堆栈管理(无需开发者介入)实现的。 --- ### 3. **载体线程(Carrier Threads)** - 虚拟线程运行时需要绑定到一个平台线程(载体线程),但 **不独占** 该线程。 - 载体线程默认使用 `ForkJoinPool`,支持任务窃取(Work-Stealing)优化调度效率。 - 当虚拟线程阻塞时,载体线程会被释放,供其他虚拟线程使用。 --- ### 4. **阻塞操作优化** - **协作式挂起**:JDK 的阻塞操作(如 `FileChannel.read`、`Socket` 等)被改造为感知虚拟线程,自动触发挂起。 - **避免占用 OS 线程**:虚拟线程的阻塞不会导致底层平台线程阻塞,从而显著提升资源利用率。 --- ### 5. **代码示例** ```java try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } ``` - 上述代码创建 1 万个虚拟线程,但实际仅需少量平台线程即可调度。 --- ### 6. **优势** - **高吞吐量**:适合 I/O 密集型任务(如网络请求、数据库访问)。 - **低成本**:内存占用小(初始约几百字节),创建速度快。 - **兼容性**:与现有 `java.lang.Thread` API 兼容,可通过 `Executors.newVirtualThreadPerTaskExecutor()` 直接使用。 --- ### 7. **局限** - **CPU 密集型任务**:若虚拟线程长时间占用 CPU,可能影响调度公平性。 - **Native 代码阻塞**:若通过 JNI 调用阻塞操作,无法自动挂起虚拟线程。 --- ### 总结 Java 虚拟线程通过 **用户态调度** 和 **Continuation 机制**,将线程生命周期管理从操作系统转移到 JVM,显著降低了高并发场景的资源消耗。它并非替代平台线程,而是与之互补,为开发者提供了一种更符合现代并发需求的轻量级工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值