什么是虚拟线程

虚拟线程是一种轻量级线程。操作系统创建并调度的线程,是一个重量级资源,其中线程切换会耗费大量CPU时间,一个系统能同时调度的线程数量是有限的。虚拟线程在很多其他语言中被称为协程、纤程、绿色线程、用户态线程等。

java中的虚拟线程

java19引入了轻量级线程:虚拟线程,java20中提供了第二个预览版本,java21中提供了正式发布,大家可以通过上面的链接,看到虚拟线程在java中的发展情况。如果需要在预览版本中使用需要添加 --enable-preview

java --source 19 --enable-preview Main.java

虚拟线程(Virtual Thread)是 java.lang.Thread 的一个实例,与平台线程(Platform Thread)类似。但虚拟线程并不与特定的操作系统线程(OS Thread)绑定。虚拟线程仍然运行在操作系统线程上,但当虚拟线程中的代码调用阻塞的 I/O 操作时,Java 运行时会挂起该虚拟线程,释放其关联的操作系统线程,使其可以执行其他虚拟线程的操作。

虚拟线程的实现类似于虚拟内存:操作系统通过将一个大的虚拟地址空间映射到有限的物理内存来模拟大量内存。同样,Java 运行时通过将大量虚拟线程映射到少量操作系统线程上来模拟大量线程。

虚拟线程的特性

1. 轻量级:

• 虚拟线程通常具有浅层调用栈,可能只执行一个 HTTP 客户端调用或一个 JDBC 查询。

• 虽然虚拟线程支持线程局部变量和可继承线程局部变量,但由于 JVM 可能支持数百万个虚拟线程,因此需要谨慎使用这些功能。

2. 阻塞场景优选:

• 虚拟线程适合运行那些大部分时间处于阻塞状态的任务,例如等待 I/O 操作完成。

• 不适合用于长时间运行的 CPU 密集型操作。

3. 高并发:

• 虚拟线程能够显著提高并发能力,因为它们不会耗尽底层操作系统线程的数量。

为什么使用虚拟线程

1. 适合高并发任务

虚拟线程非常适合用于高吞吐量的并发应用,尤其是在以下场景中:

• 存在大量并发任务。

• 这些任务大部分时间处于等待状态(如等待 I/O 操作完成)。

例如,服务器应用通常需要处理大量客户端请求,而这些请求往往涉及阻塞的 I/O 操作(如获取资源)。虚拟线程能够显著提升这些应用的并发能力。

2. 提升吞吐量

• 虚拟线程的主要优势是扩展能力(scale),即可以在 JVM 中轻松管理大量线程。

虚拟线程并不比平台线程运行得更快,它们的代码执行速度并没有提高。

• 通过减少操作系统线程的使用,可以提高整体吞吐量,因为虚拟线程能够更加高效地利用操作系统资源。

虚拟线程的目的并不是加速代码执行(提高速度或降低延迟),而是为了支持更高的并发能力(更高的吞吐量)。它提供了一种轻量级线程实现,能够帮助开发者在保持代码简单的同时实现更强的并发性能。

我们怎么使用虚拟线程

大家看参照官网文档,看看示例程序。

创建并打印的虚拟线程,使用thread.join()等待主线程结束。

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();

可以使用Thread.Builder创建虚拟线程。

Thread.Builder builder = Thread.ofVirtual().name("MyThread");
Runnable task = () -> {
    System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread t name: " + t.getName());
t.join();

更多示例可以参考上面的官方文档。

虚拟线程的挂载与卸载

• 当虚拟线程需要运行时,Java 运行时会将其挂载(mount)到一个平台线程上(称为载体线程 Carrier)。

• 操作系统调度这个载体线程,使虚拟线程运行其代码。

• 当虚拟线程执行到某些阻塞操作(如 I/O 操作)时,它会从载体线程上卸载(unmount),释放载体线程,使其可以被其他虚拟线程使用。

• 这种挂载和卸载机制提升了载体线程的利用率,并支持高并发场景。

下面情况是无法卸载的:

1. 进入 synchronized 块或方法:

• 虚拟线程运行在同步代码块或同步方法中时,会被绑定到当前的载体线程,直到同步代码执行完成。

2. 运行本地方法或外部函数:

• 当虚拟线程调用本地方法(Native Method)或外部函数(通过外部函数与内存 API 调用)时,也会被绑定。

优化建议:

• 减少频繁和长时间的绑定操作:

• 优化 synchronized 块或方法,尽量避免在这些代码块中运行长时间操作。

• 使用 java.util.concurrent.locks.ReentrantLock 替代 synchronized:

• 对可能涉及长时间阻塞(如 I/O 操作)的代码,推荐使用可中断的锁机制(如 ReentrantLock),以减少绑定的时间和频率。