Virtual Thread是JDK19中新引入的一个功能,是用户级别的线程。旨在帮助开发者以更简单、清晰的方式开发出高性能,吞吐量更大的应用程序。
以服务端应用为例,我们看一下Java中使用Thread的方式
在thread-per-request
的服务端应用中,一个request从始至终都运行在同一个线程上。这样开发出来的程序有几个特点
由于JDK的线程是在OS的线程上封装了一层,一个JDK线程对应了一个OS线程。如果线程过多会导致CPU频繁切换也会影响到系统的性能,所以我们在实际应用中通常会使用线程池来控制我们的线程数,同时减少创建、销毁线程所带来的开销。
在同步编程的方式中,同时能够处理的请求数量依赖于线程池的数量。在异步编程中采用了另一种thread-sharing
的方式,当一个请求遇到I/O操作时,它会将当前线程返还给线程池,这样该线程就可以为其他请求服务。通过异步编程的方式我们可以在资源有限的方式下开发出高性能的services,但是这也带来了不小的复杂度:
在前面的介绍中我们可以看到应用程序的开发便利性和高性能似乎不可兼得。Vritual Thread的提出就是为了解决这个问题,让开发者能够通过同步的方式进行编程,同时又能够获得异步模式的高吞吐量。
A virtual thread is an instance of java.lang.Thread that is not tied to a particular OS thread. A platform thread, by contrast, is an instance of java.lang.Thread implemented in the traditional way, as a thin wrapper around an OS thread.
virtual thread 是java.lang.Thread的子类,但是并不和特定的OS线程绑定。当我们的代码运行在virtual thread上时:
创建virtual thread的开销很低,因此绝对不要池化virtual thread,大多数virtual thread的生命周期应该伴随着一个后台task或者一个http请求而创建,结束后销毁。
我们可以通过下面的实例看一下如何使用virtual thread
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // executor.close() is called implicitly, and waits
这个例子中我们创建了10,000 个virtual thread,但是实际上创建的OS的线程可能仅有10个,那么我们指定在virtual thread运行的背后,到底有多少个真正的线程在执行?
配置 | 含义 |
---|---|
jdk.virtualThreadScheduler.parallelism | 可用于调度虚拟线程的平台线程数。它默认为可用处理器的数量 |
jdk.virtualThreadScheduler.maxPoolSize | 调度程序可用的最大平台线程数。默认为 256。 |
JDK普通线程(platform thread)的调度是借助OS来完成的,virtual thread的调度是由JDK自己实现的。JDK的调度器(实际上是一个ForkJoinPool)将virtual thread分配到给一个JDK线程(被称为carrier),再借助OS来实现JDK线程的调度。
在virtual thread的生命周期中会有多个不同的carrier,每一次挂起后再执行都有可能是不同的carrier:
注意这里并不是说不支持thread local只是相互之间不可见。同时由于virtual thread不需要池化,应当谨慎使用thread local以避免占用过多的内存。
virtual thread在运行时会mount到一个JDK线程上,当执行阻塞的I/O或者JDK其他的阻塞方法时(BlockingQueue.take())会进行unmount释放当前线程。当阻塞操作结束时,virtual thread会重新提交给线程调度器,然后mount到一个新的carrier上继续执行。
然而操作系统的限制(文件系统操作)和JDK自身的限制(Object.wait())有一些阻塞操作JDK不会进行unmount的操作,virtual thread和carrier会被同时阻塞。以下两类操作会将virtual thread固定到carrier上:
JDK提供了一些诊断工具来帮助我们判断是否有发生固定:
https://openjdk.org/jeps/444