Java虚拟线程(Virtual Threads)标志着Java在并发编程领域的一次重大飞跃,特别是从Java 21版本开始。这项新技术的引入旨在克服传统多线程和线程池存在的挑战。
多线程和线程池
在Java中,传统的多线程编程依赖于Thread
类或实现Runnable
接口。这些线程都是重量级的,因为每个线程都对应一个操作系统级的线程,这意味着线程的创建、调度和销毁都需要操作系统的深度参与,不仅耗费资源,也消耗时间。
为了优化资源使用和提高效率,Java提供了线程池(ExecutorService
等)。线程池可以重用固定数量的线程,避免了频繁创建和销毁线程的开销。然而,即使是线程池也无法完全解决上下文切换和资源消耗的问题,尤其是在高并发场景下。此外,大量的线程创建还可能导致OutOfMemoryError
。
下面是一个线程池OutOfMemoryError
的例子:
publicstaticvoidmain(String[]args){
stackOverFlowErrorExample();
}
privatestaticvoidstackOverFlowErrorExample(){
for(inti=0;i<100_000;i++){
newThread(()->{
try{
Thread.sleep(Duration.ofSeconds(1L));
}catch(InterruptedExceptione){
thrownewRuntimeException(e);
}
}).start();
}
}
虚拟线程引入
为了进一步提高并发编程的效率和简化开发过程,Java19引入了虚拟线程概念。这些轻量级的线程在JVM的用户模式下被管理,而不是直接映射到操作系统的线程上。这种设计使得可以创建数百万个虚拟线程,而对操作系统资源的消耗微乎其微。
当代码调用到阻塞操作时例如 IO、同步、Sleep等操作时,JVM 会自动把 Virtual Thread 从平台线程上卸载,平台线程就会去处理下一个虚拟线程,通过这种方式,提升了平台线程的利用率,让平台线程不再阻塞在等待上,从底层实现了少量平台线程就可以处理大量请求,提高了服务吞吐和 CPU 的利用率。
- •操作系统线程(OS Thread):由操作系统管理,是操作系统调度的基本单位。
- •平台线程(Platform Thread):传统方式使用的Java.Lang.Thread,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射。
- •虚拟线程(Virtual Thread):一种轻量级,由 JVM 管理的线程。对应的实例 java.lang.VirtualThread 这个类。
- •载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程。
使用虚拟线程
虚拟线程的使用接口与普通线程相似,但创建虚拟线程的方式略有不同。以下是几种创建和使用虚拟线程的方法:
- 直接创建虚拟线程并运行:
//传入Runnable实例并立刻运行:
Threadvt=Thread.startVirtualThread(()->{
System.out.println("Startvirtualthread...");
Thread.sleep(10);
System.out.println("Endvirtualthread.");
});
- 创建虚拟线程但不自动运行,而是手动调用start()开始运行:
//创建VirtualThread:
Thread.ofVirtual().unstarted(()->{
System.out.println("Startvirtualthread...");
Thread.sleep(1000);
System.out.println("Endvirtualthread.");
});
//运行:
vt.start();
- 通过虚拟线程的ThreadFactory创建虚拟线程,然后手动调用start()开始运行:
//创建ThreadFactory:
ThreadFactorytf=Thread.ofVirtual().factory();
//创建VirtualThread:
Threadvt=tf.newThread(()->{
System.out.println("Startvirtualthread...");
Thread.sleep(1000);
System.out.println("Endvirtualthread.");
});
//运行:
vt.start();
直接调用start()实际上是由ForkJoinPool的线程来调度的。我们也可以自己创建调度线程,然后运行虚拟线程:
ExecutorServiceexecutor=Executors.newVirtualThreadPerTaskExecutor();
//创建大量虚拟线程并调度:
ThreadFactorytf=Thread.ofVirtual().factory();
for(inti=0;i<100_000;i++){
Threadvt=tf.newThread(()->{...});
executor.submit(vt);
executor.submit(()->{
System.out.println("Startvirtualthread...");
Thread.sleep(Duration.ofSeconds(1L));
System.out.println("Endvirtualthread.");
returntrue;
});
}
由于虚拟线程属于非常轻量级的资源,因此,用时创建,用完就扔,不要池化虚拟线程。
虚线程的性能
下面我们测试一下虚线程的性能
publicstaticvoidmain(String[]args){
testWithVirtualThread();
testWithThread(20);
testWithThread(50);
testWithThread(100);
testWithThread(200);
testWithThread(400);
}
privatestaticlongtestWithVirtualThread(){
longstart=System.currentTimeMillis();
ExecutorServicees=Executors.newVirtualThreadPerTaskExecutor();
for(inti=0;i<TASK_NUM;i++){
es.submit(()->{
Thread.sleep(100);
return0;
});
}
es.close();
longend=System.currentTimeMillis();
System.out.println("virtualthread:"+(end-start));
returnend;
}
privatestaticvoidtestWithThread(intthreadNum){
longstart=System.currentTimeMillis();
ExecutorServicees=Executors.newFixedThreadPool(threadNum);
for(inti=0;i<TASK_NUM;i++){
es.submit(()->{
Thread.sleep(100);
return0;
});
}
es.close();
System.out.println(threadNum+"thread:"+(System.currentTimeMillis()-start));
es.shutdown();
}
下面是测试结果:
虚线程真是快到飞起!!!
虚拟线程的原理
Java的虚拟线程会把任务(java.lang.Runnable实例)包装到一个Continuation
实例中。当任务需要阻塞挂起的时候,会调用Continuation
的 yield 操作进行阻塞,虚拟线程会从平台线程卸载。 当任务解除阻塞继续执行的时候,调用Continuation.run
会从阻塞点继续执行。下面让我们结合Thread.ofVirtual().start()
来看一下虚线程的实现。
当调用start()方法时,会创建一个虚拟线程varthread =newVirtualThread(scheduler,nextThreadName(),characteristics(),task);
staticThreadnewVirtualThread(Executorscheduler,
Stringname,
intcharacteristics,
Runnabletask){
if(ContinuationSupport.isSupported()){
returnnewVirtualThread(scheduler,name,characteristics,task);
}else{
if(scheduler!=null)
thrownewUnsupportedOperationException();
returnnewBoundVirtualThread(name,characteristics,task);
}
}
核心主要在java.lang.VirtualThread类中。下面是JVM 调用VirtualThread的构造函数:
VirtualThread会初始化一个ForkJoinPool的Executor
.
private static final ForkJoinPool DEFAULT_SCHEDULER = createDefaultScheduler(); 该方法初始化Executor线程池大小。该Executor也就是执行器,提供了一个默认的 FIFO 的 ForkJoinPool 用于执行虚拟线程任务。
之后创建一个VThreadContinuation对象。该对象存储作为Runnable对象运行的信息,它确保了每个并发操作都有清晰定义的生命周期和上下文。
“`VThreadContinuation是一种允许程序执行被暂停并在将来某个时刻恢复的机制。虚拟线程利用
VThreadContinuation`来实现轻量级的上下文切换.
最后,该方法调用runContinuation方法。该方法在虚拟线程启动时被调用。
JVM 把虚拟线程分配给平台线程的操作称为 mount(挂载),取消分配平台线程的操作称为 unmount(卸载)。
Continuation组件十分重要,它既是用户真实任务的包装器,同时提供了虚拟线程任务暂停/继续的能力,以及虚拟线程与平台线程数据转移功能,当任务需要阻塞挂起的时候,调用Continuation的 yield 操作进行阻塞。当任务需要解除阻塞继续执行的时候,则调用Continuation的 run 恢复执行。
总结
虚拟线程是由 Java 虚拟机调度,它的占用空间小,同时使用轻量级的任务队列来调度虚拟线程,避免了线程间基于内核的上下文切换开销,因此可以极大量地创建和使用。主要有以下好处:
- 虚拟线程是轻量级的,它们不直接映射到操作系统的线程,而是由JVM在用户态进行管理。这种轻量级特性允许在单个JVM实例中同时运行数百万个虚拟线程。
- 虚拟线程大大简化了并发编程的复杂性。开发者可以像编写顺序代码一样编写并发代码,而无需担心传统线程编程中的许多复杂问题,如线程数、同步和资源竞争等。