Java21 虚拟线程
虚拟线程(Virtual Thread)
虚拟线程是JDK 21中最受关注且对并发编程影响深远的特性。虚拟线程极大地降低了创建和管理线程的成本,允许开发人员轻松地编写和维护高并发应用程序。由于其轻量级特性,开发者可以创建大量的并发任务,而不会像传统线程那样面临线程上下文切换和内存消耗等问题。虚拟线程与Go语言中的协程类似,使得编写高并发、高吞吐量的应用程序变得更加简单和高效,尤其在处理I/O密集型任务时,能够显著提升系统的并发能力。
传统线程和虚拟线程
创建操作系统线程(传统线程):
Thread thread = new Thread(() -> {System.out.println("Hello, World!");
});
- 调度层次:此代码创建了一个标准的Java平台线程(即操作系统线程),它是由Java语言提供的java.lang.Thread类实例化并启动的。这类线程是操作系统内核直接调度和管理的实体,与操作系统的原生线程一一对应。它们的生命周期、调度和上下文切换由底层操作系统负责。
- 资源消耗:操作系统线程需要占用较多资源(内存、CPU等),而且每次创建和销毁线程都比较费时费力。如果创建大量线程,会增加系统负担。
- 并发能力:操作系统线程可以并发执行任务,但当线程数很多时,会导致操作系统的限制和频繁的上下文切换,反而降低性能。
- 阻塞行为:如果线程因等待I/O等操作而阻塞,它会释放CPU给其他线程使用,但仍占用系统资源。大量阻塞线程会浪费资源,降低效率。
- API与语义:可以通过 new Thread() 来创建线程,并用 start() 方法启动它。可以用Lambda表达式传递任务给线程。
创建Java虚拟线程:
Thread virtualThread = Thread.startVirtualThread(() -> {System.out.println("Hello, World!");
});
- 调度层次:这段代码创建了一个Java虚拟线程,它是Java平台在JDK 19发布的,虚拟线程不直接对应操作系统线程,JVM通过少量的平台线程来调度大量的虚拟线程,避免了传统线程的开销。
- 资源消耗:创建和销毁虚拟线程的成本非常低,几乎不消耗内存,适合处理成千上万的并发任务。
- 并发能力:由于虚拟线程的轻量化特性,它们能提供更高的并发能力,尤其在处理大量I/O阻塞任务时非常高效。
- 阻塞行为:虚拟线程在阻塞时不会占用平台线程,JVM会将它们从平台线程解绑,避免浪费资源,并在需要时重新绑定到可用平台线程。
- API与语义:使用静态方法Thread.startVirtualThread()直接创建并启动一个虚拟线程,同样传入一个Lambda表达式作为线程的Runnable任务。注意,这是针对Project Loom新增的API,反映了虚拟线程特有的创建和启动方式。
有了虚拟线程,线程池还有必要存在吗?
创建线程
就像是 招人,而 线程池
是一群 已经招进来的工人。
多线程
是 多个工人 在做不同的工作,线程池
则让 一个工人 可以在不同任务之间 灵活切换。
密集I/O
是工人空闲时间多,密集计算
是工人需要长时间集中工作。
虚拟线程
则能够让工人在任务空闲时,快速切换到其他任务,提高效率,特别适合 I/O密集型 操作。
什么情况需要保留线程池呢?
虚拟线程的引入确实极大的降低创建和管理线程的开销,使用并发编程更高效便捷,但并不意味着多线程或线程弛就变得多余。
1、任务管理:线程池提供了任务队列,线程复用,拒绝策略等高级功能,这些特性对应管理任务类型线程(包括虚拟线程)的工作负载都是至关重要的。列如:可以通过线程池设计最大并发数,超时处理,异常处理等
2、声明周期控制:线程池允许开发者更精细的控制线程的生命周期,在完成一系列任务后可以优雅的关闭和资源回收。即便虚拟线程简化创建和销毁,但某些场景仍需要有组织地结束并清理资源。
3、兼容性和迁移成本:目前大量java库和架构都是基于线程池设计和实现,直接切换成虚拟线程的代码修改和测试成本太大。
如何使用虚拟线程?
从 Java 19 开始,虚拟线程作为预览特性引入,并在 Java 21 中成为正式特性。可以通过 java.util.concurrent
包下的 ExecutorService
来创建和管理虚拟线程。
1. 启动虚拟线程
可以使用 Thread.ofVirtual().start()
来创建一个虚拟线程并启动它。代码示例如下:
public class VirtualThreadExample {public static void main(String[] args) {// 创建并启动一个虚拟线程Thread virtualThread = Thread.ofVirtual().start(() -> {System.out.println("Hello 虚拟线程!");});// 等待虚拟线程执行完毕try {virtualThread.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}
2. 使用虚拟线程池
通常情况下,我们不直接管理虚拟线程,而是使用线程池来统一管理线程。Java 提供了新的虚拟线程池支持,可以通过 Executors.newVirtualThreadPerTaskExecutor()
来创建一个虚拟线程池。这样,每个任务都会在一个独立的虚拟线程中执行。
import java.util.concurrent.*;public class VirtualThreadPoolExample {public static void main(String[] args) {// 创建一个虚拟线程池ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();// 提交任务到线程池for (int i = 0; i < 5; i++) {final int taskId = i;executorService.submit(() -> {System.out.println("任务" + taskId + "===虚拟线程正在执行");});}// 关闭线程池executorService.shutdown();}
}
3. 使用 StructuredTaskScope
StructuredTaskScope
是 Java 21 中引入的用于简化并发控制的一个 API,它可以用于管理一组任务并确保它们在正确的时间完成。
import java.util.concurrent.*;public class VirtualThreadScopeExample {public static void main(String[] args) {try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {// 启动并发任务scope.fork(() -> {System.out.println("虚拟线程正在执行任务一");});scope.fork(() -> {System.out.println("虚拟线程正在执行任务二");});// 等待所有任务完成scope.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}
区别:
并发性能对比
假设我们要测试在高并发场景下,虚拟线程和传统线程的性能差异。下面的示例演示了如何在两种不同的线程池中并行执行大量任务。
传统线程池并发性能
import java.util.concurrent.*;public class TraditionalThreadPoolPerformance {public static void main(String[] args) throws InterruptedException {long startTime = System.nanoTime();// 创建一个固定大小的传统线程池ExecutorService executorService = Executors.newFixedThreadPool(4);for (int i = 0; i < 1_000_000; i++) {executorService.submit(() -> {// 模拟任务try {Thread.sleep(1);} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.MINUTES);long endTime = System.nanoTime();System.out.println("传统线程执行时间:" + (endTime - startTime) / 1_000_000 + " ms");}
}
- 这里创建了一个固定大小为 4 的传统线程池,提交 100 万个任务。由于线程池大小固定,任务会排队等待空闲线程,导致性能瓶颈。
虚拟线程池并发性能
import java.util.concurrent.*;public class VirtualThreadPoolPerformance {public static void main(String[] args) throws InterruptedException {long startTime = System.nanoTime();// 创建一个虚拟线程池ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();for (int i = 0; i < 1_000_000; i++) {executorService.submit(() -> {// 模拟任务try {Thread.sleep(1);} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}executorService.shutdown();executorService.awaitTermination(1, TimeUnit.MINUTES);long endTime = System.nanoTime();System.out.println("虚拟线程执行时间:" + (endTime - startTime) / 1_000_000 + " ms");}
}
- 使用虚拟线程池,虚拟线程池不会有传统线程池中的线程数限制,所有任务都会立即分配一个虚拟线程执行,因此能够更高效地处理大量并发任务。
使用虚拟线程的场景
虚拟线程非常适合以下场景:
- 高并发的 I/O 密集型任务:虚拟线程特别适合处理大量并发的 I/O 操作,例如网络请求、数据库操作等,因为它们的创建和管理开销小,可以支持更多的并发任务。
- 简单的任务并发:对于简单的任务,并发处理不需要复杂的线程池和任务调度,虚拟线程可以提供更简洁的编程模型。
- 减少上下文切换的开销:与传统线程相比,虚拟线程的上下文切换开销较低,可以提高系统的吞吐量和响应性。