什么是虚拟线程
在虚拟线程(Virtual Threads)出现之前,Java中只有一个Thread(虚拟线程出来后也被叫做平台线程PlatformThread)。一般来说一个PlatformThread对应一个操作系统线程,所以平台线程的数量受限于操作系统的线程数量上限。
Java SE最常用的JVM是Oracle/Sun研发的HotSpot VM。在这个JVM的较新版本所支持的所有平台上,它都是使用1:1线程模型的——除了Solaris之外,它是个特例。
其实JVM规范里并没有规定是按1:1线程模型,但绝大多数都是1:1的。具体请参考R大的回复:对JVM中的线程模型是用户级的么?
虚拟线程在JEP5425
提案中作为Preview
功能在JDK19中出现,并在JDK21中以JEP444
提案作为正式功能release。
JEP444
详细记录了虚拟线程相关信息,非常值得阅读和参考。https://openjdk.org/jeps/444
Virtual threads are lightweight threads that dramatically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications.
官方定性: 虚拟线程是能显著减少编写、维护和观察高吞吐量应用的轻量级线程。
Virtual threads are a lightweight implementation of threads that is provided by the JDK rather than the OS. They are a form of user-mode threads, which have been successful in other multithreaded languages (e.g., goroutines in Go and processes in Erlang). User-mode threads even featured as so-called “green threads” in early versions of Java, when OS threads were not yet mature and widespread. However, Java’s green threads all shared one OS thread (M:1 scheduling) and were eventually outperformed by platform threads, implemented as wrappers for OS threads (1:1 scheduling). Virtual threads employ M:N scheduling, where a large number (M) of virtual threads is scheduled to run on a smaller number (N) of OS threads.
To run code in a virtual thread, the JDK’s virtual thread scheduler assigns the virtual thread for execution on a platform thread by mounting the virtual thread on a platform thread. This makes the platform thread become the carrier of the virtual thread.
个人理解,虚拟线程和go中goroutine其实本质差不多,都是用户级轻量级线程调度(协程的概念)。在Java中,多个虚拟线程运行在同一个平台线程上,由平台线程自己调度虚拟线程的挂起和执行(M:N 调度,大量的虚拟线程(M)被调度在较少数量的操作系统线程(N)上运行)。所以平台线程也可以称作是虚拟线程的载体(carrier
)。
这些虚拟线程的堆栈存储在Java的垃圾收集堆上,可以被正常gc的,创建虚拟线程的开销非常小(虚拟线程的线程栈不同于平台线程,不属于gc roots)。而且因为是用户态线程管理调度,所以也不需要像传统线程一样需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换,也就是不存在所谓的上下文切换。
虚拟线程的几个目标就是:
- 可以用最简单的
thread-per-request
这样的风格来编程。(不用再去写异步的方式来挺高并发和吞吐能力) - 使用 java.lang.Thread API 的现有代码能够以最小的更改采用虚拟线程。(设计真的很合理,基本就是对原来Thread的扩展,平台线程支持的能力虚拟线程都支持,如局部变量、线程中断等等)
- 使用现有 JDK 工具轻松进行虚拟线程故障排除、调试和分析。(即使是虚拟线程,也基本可以当作是PlatformThread来定位排障)
关于虚拟线程及其使用
快速新建虚拟线程任务并执行:
Thread.startVirtualThread(() -> System.out.println("虚拟线程执行中..."));
Thread.ofVirtual().start(() -> System.out.println("虚拟线程执行中..."));
Thread.ofVirtual().unstarted(() -> System.out.println("虚拟线程执行中...")).start();
通过创建ThreadFactory
创建虚拟线程
var virtualFactory = Thread.ofVirtual().factory();
virtualFactory.newThread(() -> System.out.println("虚拟线程执行中...")).start();
针对一些线程池化的业务,也可以用Executors.newVirtualThreadPerTaskExecutor()
代替原有线程池。
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
结构化并发(Structured Concurrency)相关的API也支持创建和管理虚拟线程,结构化并发也更好的编排线程之间的关系(包括虚拟线程和平台线程)。但是结构化并发相关API还处于孵化阶段,还没正式release,这里就不多介绍了。
虚拟线程使用时需要注意的点
软件工程里不存在银弹,虚拟线程在使用的过程中也遇到了一些需要注意的点。
- 不要池化虚拟线程
- 虚拟线程相当的cheap,池化虚拟线程反而会导致并发起不来
- ExecutorService.shutdown()对于虚拟线程来说,并不是阻塞的,会立即返回。
- 对于已提交的任务,shutDown()不会终止已提交的任务,shutDownNow()会终止。
var executor = Executors.newVirtualThreadPerTaskExecutor(); for (int i = 0; i < 10; i++) { int finalI = i; executor.execute(() -> { try { Thread.sleep(Duration.ofSeconds(2)); log.info("虚拟线程替换线程池执行中..." + finalI); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } // shutdownNow()和shutdown()的区别,和`ThreadPoolExecutor`的定义和返回是有区别的 //executor.shutdownNow(); executor.shutdown(); Thread.currentThread().join(10 * 1000);
- 对于已提交的任务,shutDown()不会终止已提交的任务,shutDownNow()会终止。
-
虚拟线程本身是支持
ThreadLocal
和InheritableThreadLocal
的(对载体线程完全无感,和平台线程使用感受一致),但是虚拟线程非常廉价,可能存在非常非常多的虚拟线程,所以使用ThreadLocal时要千万小心。后续Scoped values
可能是代替的一个更好选择,不过目前还在孵化中。 - 通常情况下,虚拟线程在阻塞I/O或其他阻塞操作时,会被unmount,下面两种情况下,虚拟线程无法被卸载,会被固定到运行它的载体上,感觉这也是使用work-stealing的FokrJoinPool来调度的理由之一(Continuation可以yieldReentrantLock的lock):
- 调用native方法或Foreign Function(外部函数)时
- 执行synchronized方法或者代码块(可以使用ReentrantLock代替,因为虚拟线程支持
LockSupport
的相关API)
虚拟线程的源码设计
virtual thread = Continuation(Runnable + ContinuationScope) + Scheduler(一个单独的ForkJoinPool,不是commonPool)
Continuation可以理解为是对虚拟线程要执行的任务的包装,提供了虚拟线程yield/run,mount/unmount的能力。
- 当任务阻塞需要挂起时,会执行Continuation.yield(ContinuationScope scope)方法,虚拟线程unmount;
- 当任务恢复时,会调用 Continuation.run 会从阻塞点之后继续执行。
Scheduler可以理解为负责调度和提供实际执行任务的载体(carrier),其本身是一个ForkJoinPool(并不是Stream并行计算时用的那个commonPool!)
实际使用虚拟线程的感受
- 如果你的业务本身就是异步编程时,性能上提升可能并不多。
- 但是相对于异步编程,虚拟线程的
thread-per-request
风格,代码更简单易懂,符合直觉,且调试也更简单。 - 结构化并发出来后,对于线程(包括虚拟线程和平台线程)的管理和编排会更加的便捷方便,但目前还处在孵化中。
- 但是相对于异步编程,虚拟线程的
-
虚拟线程真的很廉价,针对的是IO密集操作,提升很明显。例如rpc查询、db查询等等。
- JDK21刚出不是很久,很多第三方库还没支持到JDK21,第三方框架没支持JDK21的情况下会有bug。