今天Java19 正式发布,带来了一个 Java 开发者垂涎已久的新特性—— 虚拟线程。在 Java 有这个新特性之前,Go语言的协程风靡已久,在并发编程领域可以说是叱咤风云。随着国内 Go 语言的快速发展与推广,协程好像成为了一个世界上最好语言的必备特性之一。Java19 虚拟线程就是来弥补这个空白的。本文将通过对虚拟线程的介绍,以及与 Go 协程的对比来带大家尝鲜 Java19 虚拟线程。
本文要点:
- Java 线程模型
- 平台线程与虚拟线程性能对比
- Java 虚拟线程与 Go 协程对比
- 如何使用虚拟线程
Java 线程模型
java 线程 与 虚拟线程
我们常用的 Java 线程与系统内核线程是一一对应的,系统内核的线程调度程序负责调度 Java 线程。为了增加应用程序的性能,我们会增加越来越多的 Java 线程,显然系统调度 Java 线程时,会占据不少资源去处理线程上下文切换。
近几十年来,我们一直依赖上述多线程模型来解决 Java 并发编程的问题。为了增加系统的吞吐量,我们要不断增加线程的数量,但机器的线程是昂贵的、可用线程数量也是有限的。即使我们使用了各种线程池来最大化线程的性价比,但是线程往往会在 CPU、网络或者内存资源耗尽之前成为我们应用程序的性能提升瓶颈,不能最大限度的释放硬件应该具有的性能。
为了解决这个问题 Java19 引入了虚拟线程(Virtual Thread)。在 Java19 中,之前我们常用的线程叫做平台线程(platform thread),与系统内核线程仍然是一一对应的。其中大量(M)的虚拟线程在较小数量(N)的平台线程(与操作系统线程一一对应)上运行(M:N调度)。多个虚拟线程会被 JVM 调度到某一个平台线程上执行,一个平台线程同时只会执行一个虚拟线程。
创建 Java 虚拟线程
新增线程相关 API
Thread.ofVirtual()
和Thread.ofPlatform()
是创建虚拟和平台线程的新API:
//输出线程ID 包括虚拟线程和系统线程 Thread.getId() 从jdk19废弃
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
//创建虚拟线程
Thread thread = Thread.ofVirtual().name("testVT").unstarted(runnable);
testVT.start();
//创建虚平台线程
Thread testPT = Thread.ofPlatform().name("testPT").unstarted(runnable);
testPT.start();
使用Thread.startVirtualThread(Runnable)
快速创建虚拟线程并启动:
//输出线程ID 包括虚拟线程和系统线程
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread thread = Thread.startVirtualThread(runnable);
Thread.isVirtual()
判断线程是否为虚拟线程:
//输出线程ID 包括虚拟线程和系统线程
Runnable runnable = () -> System.out.println(Thread.currentThread().isVirtual());
Thread thread = Thread.startVirtualThread(runnable);
Thread.join
和Thread.sleep
等待虚拟线程结束、使虚拟线程 sleep:
Runnable runnable = () -> System.out.println(Thread.sleep(10));
Thread thread = Thread.startVirtualThread(runnable);
//等待虚拟线程结束
thread.join();
Executors.newVirtualThreadPerTaskExecutor()
创建一个 ExecutorService,该 ExecutorService 为每个任务创建一个新的虚拟线程:
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
运行程序;
是骡子是马
既然是为了解决平台线程的问题,那我们就直接测试平台线程与虚拟线程的性能。
测试内容很简单,并行执行一万个 sleep 一秒的任务,对比总的执行时间和所用系统线程数量。
为了监控测试所用系统线程的数量,编写如下代码:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
System.out.println(threadInfo.length + " os thread");
}, 1, 1, TimeUnit.SECONDS);
调度线程池每一秒钟获取并打印系统线程数量,便于观察线程的数量。
public static void main(String[] args) {
//记录系统线程数
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleAtFixedRate(() -> {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfo = threadBean.dumpAllThreads(false, false);
System.out.println(threadInfo.length + " os thread");
}, 1, 1, TimeUnit.SECONDS);
long l = System.currentTimeMillis();
try(var executor = Executors.newCachedThreadPool()) {
IntStream.range(0,