本文将向您介绍虚拟线程,以尝试 Java 19 虚拟线程。
Java线程和虚拟线程
我们常用的 java 线程和系统内核 线程 是一一对应的,系统内核的线程调度器负责调度Java线程。
为了提高应用程序的性能,我们将添加越来越多的 Java 线程。系统在调度Java线程时,会占用大量资源来处理线程上下文切换。
几十年来,我们一直依靠上述多线程模型来解决 Java 中的并发编程问题。为了提高系统的吞吐量,我们不得不不断的增加线程的数量,但是机器的线程是昂贵的,可用的线程数量是有限的。
即使我们使用各种 线程池 来最大化线程的成本效益,但线程往往在 CPU 、网络或内存资源耗尽之前成为我们应用程序的性能提升瓶颈,无法最大化硬件应有的性能。
为了解决这个问题, Java19 引入了虚拟线程。 在Java19中,我们之前使用的线程被称为平台线程,仍然与系统内核线程一一对应。大量 (M) 的虚拟线程运行在较少数量 (N) 的平台线程上(与操作系统线程一一对应)(M:N 调度)。 JVM 会调度多个虚拟线程在某个平台线程上执行,一个平台线程同时只会执行一个虚拟线程。
创建 Java 虚拟线程
Thread.ofVirtual() 并且 Thread.ofPlatform() 是用于创建虚拟和平台线程的新 API:
// Thread.getId() from jdk19 abandoned
Runnable runnable = () -> System.out.pr Int ln(Thread.currentThread().threadId());
Thread thread = Thread.ofVirtual().name("testVT").unstarted(runnable);
Thread testPT = Thread.ofPlatform().name("testPT").unstarted(runnable);
testPT.start();
用于 Thread.startVirtualThread(Runnable) 快速创建和启动虚拟线程:
Runnable runnable = () -> System.out.println(Thread.currentThread().threadId());
Thread thread = Thread.startVirtualThread(runnable);
Thread.isVirtual() 判断一个线程是否为虚拟线程:
Runnable runnable = () -> System.out.println(Thread.currentThread().isVirtual());
Thread thread = Thread.startVirtualThread(runnable);
Thread.join 并 Thread.sleep 等待虚拟线程结束并使虚拟线程进入睡眠状态:
Runnable runnable = () -> System.out.println(Thread.sleep(10));
Thread thread = Thread.startVirtualThread(runnable);
thread.join();
Executors.newVirtualThreadPerTaskExecutor() 创建一个 ExecutorService 为每个任务创建一个新的虚拟线程:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("hello"));
}
支持使用线程池和 ExecutorService .
注意事项 :
测试平台线程和虚拟线程的性能
测试内容很简单。并行执行 10000 个休眠一秒的任务,比较总执行时间和使用的系统线程数。
测试内容很简单。并行执行 10000 个休眠一秒的任务,比较总执行时间和使用的系统线程数。
要监控测试使用的系统线程数,请编写以下代码:
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, 10000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(i);
return i;
});
});
}
System.out.printf("elapsed time:%d ms", System.currentTimeMillis() - l);
}
1
7142
3914 os thread
Exception in thread "main" java.lang. OutOfMemory Error: unable to create native thread: possibly out of memory or process/resource limits reached
at java.base/java.lang.Thread.start0(Native Method)
at java.base/java.lang.Thread.start(Thread.java:1560)
at java.base/java.lang.System$2.start(System.java:2526)
首先,我们 Executors.newCachedThreadPool() 用来执行10000个任务,因为最大线程数 newCachedThreadPool 是 Integer.MAX_VALUE ,所以理论上至少会创建几千个系统线程来执行。
从上面的输出可以看出,最多创建了3914个系统线程,然后继续创建线程时出现异常,程序终止。我们通过大量的系统线程来提高系统的性能是不现实的,因为线程昂贵且资源有限。
现在我们使用一个固定大小为 200 的线程池来解决不应用太多系统线程的问题:
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.newFixedThreadPool(200)) {
IntStream.range(0, 10000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(i);
return i;
});
});
}
System.out.printf("elapsed time: %dmsn", System.currentTimeMillis() - l);
}
1
9987
9998
207 os thread
elapsed time: 50436ms
使用固定大小的线程池后,不存在创建大量系统线程导致失败的问题,可以正常完成任务。最多创建207个系统线程,总共耗时50436ms。
我们来看看使用虚拟线程的结果:
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");
}, 10, 10, TimeUnit.MILLISECONDS);
long l = System.currentTimeMillis();
try(var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
System.out.println(i);
return i;
});
});
}
System.out.printf("elapsed time: %dmsn", System.currentTimeMillis() - l);
}
1
9890
15 os thread
elapsed time: 1582ms
使用虚拟线程的代码比使用固定大小少一个字,替换 Executors.newFixedThreadPool(200) 为 Executors.newVirtualThreadPerTaskExecutor() .
从输出可以看出,总执行时间为1582 ms,最多使用了15个系统线程。结论很清楚,虚拟线程比平台线程快得多,并且使用更少的系统线程资源。
如果我们将刚才测试程序中的任务替换为执行一秒计算(例如对一个巨大的数组进行排序),而不是仅仅休眠一秒,即使我们把虚拟线程或平台线程的数量也增加了远大于处理器内核的数量不会产生显着的性能提升。
因为虚拟线程不是更快的线程,所以它们在运行代码方面没有比平台线程更快的优势。虚拟线程的存在是为了提供更高的吞吐量,而不是速度(更低的延迟)。
如果您的应用程序满足以下两个特征,则使用虚拟线程可以显着提高程序吞吐量:
- 该程序具有大量并发任务。
- IO 密集型,工作负载不受 CPU 限制。
虚拟线程可以帮助提高具有大量并发性的服务器端应用程序的吞吐量,而这些任务通常有大量的 IO 等待。