近期将 ad-filters-subscriber 升级到了 Java21,
借此体验了一下终于正式推出的虚拟线程。这篇文章就顺便记录一下关于虚拟线程的若干测试用例。
Java 虚拟线程(Virtual Threads)是 JVM 内部轻量级线程实现,于Java 19 引入,Java 21 正式发布。
特点:
轻量级
不需要操作系统内核参与,创建和上下文切换成本远低于传统的操作系统线程(即平台线程),且占用的内存资源较少。
高性能
由于虚拟线程的轻量化特性,应用程序可以轻松创建和管理成千上万的并发任务,尤其适用于高并发、I/O密集型或大量短生命周期任务的场景。
灵活调度
虚拟线程的调度完全在用户空间(Java Runtime)进行,不受操作系统内核调度的限制。JVM 可以根据应用需求和系统负载更精细地控制虚拟线程的执行。
平台线程的映射
虚拟线程并非直接对应操作系统线程。JVM会将多个虚拟线程映射到少量的平台线程上,通过协作式多任务(如协程、纤程)技术,
让多个虚拟线程在同一个平台线程上轮流执行,从而避免频繁的上下文切换。
标准 API 集成
虚拟线程以标准 Java API 的形式提供,可以直接使用 java.lang.Thread
类创建和管理虚拟线程。
创建方式
根据前文介绍,虚拟线程以标准Java API的形式提供,因此其创建也相当简单,简单来说有4种方式:
1. 直接创建并执行
1
2
3
4
Thread vt = Thread . startVirtualThread (() -> {
Thread . currentThread (). setName ( "virtual thread" );
log . info ( "create and start virtual thread" );
});
2. 通过构造器
1
2
3
4
5
Thread vt = Thread . ofVirtual (). unstarted (() -> {
Thread . currentThread (). setName ( "virtual thread" );
log . info ( "create and manual start virtual thread" );
});
vt . start ();
3. 通过线程工厂
1
2
3
4
5
6
ThreadFactory factory = Thread . ofVirtual (). factory ();
Thread vt = factory . newThread (() -> {
Thread . currentThread (). setName ( "virtual thread" );
log . info ( "create virtual thread by factory" );
});
vt . start ();
4. 通过线程池
1
2
3
4
5
6
7
// 等同 newThreadPerTaskExecutor(ThreadFactory)
try ( ExecutorService executor = Executors . newVirtualThreadPerTaskExecutor ()) {
executor . execute (() -> {
Thread . currentThread (). setName ( "virtual thread" );
log . info ( "create virtual thread by executor" );
});
}
始终是守护线程
既然虚拟线程与平台线程没有直接联系,那么可以先验证一下其是否是并行的,建立如下用例,为了方便展示直接写成 main 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main ( String [] args ) throws InterruptedException {
log . info ( "main thread start" );
Thread . startVirtualThread (() -> {
Thread . currentThread (). setName ( "virtual thread-1" );
log . info ( "thread-1 start" );
try {
Thread . sleep ( 3000 );
} catch ( InterruptedException e ) {
throw new RuntimeException ( e );
}
log . info ( "thread-1 end" );
});
Thread . startVirtualThread (() -> {
Thread . currentThread (). setName ( "virtual thread-2" );
log . info ( "thread-2 start" );
try {
Thread . sleep ( 2000 );
} catch ( InterruptedException e ) {
throw new RuntimeException ( e );
}
log . info ( "thread-2 end" );
});
Thread . sleep ( 6000 );
log . info ( "main thread end" );
}
用例内容很直观:分别建立并运行两个虚拟线程,一个休眠3秒,一个休眠2秒。
如果两个虚拟线程是并行,那么两虚拟线程应该同时执行,且 thread-2
先于thread-1
1秒结束。
如不是并行,那么 thread-1
先开始于3秒后结束,thread-2
才开始。
接下来看下执行结果:
1
2
3
4
5
6
14:37:16.051 [ main] -- main thread start
14:37:16.061 [ virtual thread-1] -- thread-1 start
14:37:16.061 [ virtual thread-2] -- thread-2 start
14:37:18.071 [ virtual thread-2] -- thread-2 end
14:37:19.066 [ virtual thread-1] -- thread-1 end
14:37:22.064 [ main] -- main thread end
结果符合预期。
但是,不难注意到代码后面有一个 Thread.sleep(6000)
,为什么要阻塞主线程呢?
先把 Thread.sleep(6000)
注释掉再执行一次看看:
1
2
3
4
14:53:32.175 [ main] -- main thread start
14:53:32.187 [ main] -- main thread end
14:53:32.187 [ virtual thread-1] -- thread-1 start
14:53:32.187 [ virtual thread-2] -- thread-2 start
可以看到两个虚拟线程开始的同时,主线程也直接结束了,然后执行就结束了…
这似乎与常识不符,主线程结束并不应该影响到其他线程。那么把虚拟线程替换为常规线程再看看结果:
1
2
3
4
5
6
14:57:36.289 [ main] -- main thread start
14:57:36.292 [ virtual thread-1] -- thread-1 start
14:57:36.293 [ main] -- main thread end
14:57:36.293 [ virtual thread-2] -- thread-2 start
14:57:38.306 [ virtual thread-2] -- thread-2 end
14:57:39.304 [ virtual thread-1] -- thread-1 end
两相对比之下不难验证,虚拟线程始终是守护线程 ,所以 JVM 会在所有常规线程完成后退出,
因此在虚拟线程中通过 Thread.currentThread().isDaemon()
可以得到 true
。
而若是执行:Thread.currentThread().setDaemon(false)
将得到一个 IllegalArgumentException: 'false' not legal for virtual threads
。
ThreadLocal
虚拟线程也可以使用 ThreadLocal 吗?简单修改一下用例,验证一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ThreadLocal < String > threadLocal = new ThreadLocal <>();
threadLocal . set ( "I am main thread" );
log . info ( "get thread local: {}" , threadLocal . get ());
Thread . startVirtualThread (() -> {
Thread . currentThread (). setName ( "virtual thread-1" );
log . info ( "get thread local: {}" , threadLocal . get ());
threadLocal . set ( "I am thread-1" );
log . info ( "get thread local: {}" , threadLocal . get ());
try {
Thread . sleep ( 3000 );
} catch ( InterruptedException e ) {
throw new RuntimeException ( e );
}
});
Thread . startVirtualThread (() -> {
Thread . currentThread (). setName ( "virtual thread-2" );
log . info ( "get thread local: {}" , threadLocal . get ());
threadLocal . set ( "I am thread-2" );
log . info ( "get thread local: {}" , threadLocal . get ());
try {
Thread . sleep ( 2000 );
} catch ( InterruptedException e ) {
throw new RuntimeException ( e );
}
});
Thread . sleep ( 6000 );
看下结果:
1
2
3
4
5
14:57:22.362 [ main] -- get thread local: I am main thread
14:57:22.374 [ virtual thread-1] -- get thread local: null
14:57:22.374 [ virtual thread-2] -- get thread local: null
14:57:22.374 [ virtual thread-1] -- get thread local: I am thread-1
14:57:22.374 [ virtual thread-2] -- get thread local: I am thread-2
结果显然是支持的。
但是使用应当谨慎,虚拟线程创建开销很小,甚至单机可以达到百万数量级,不当的使用 ThreadLocal 对内存来说无疑是一场灾难。
虚拟线程和Spring
在 spring 中,进行异步调用一般会使用 @Async
注解,它会将任务提交到默认的 TaskExecutor
线程池中以异步执行。
那么,能否使用虚拟线程来替代呢?
创建一个 spring-boot 程序来测试一下,首先需要在启动类上加入 @EnableAsync
注解,而后定义如下bean:
1
2
3
4
5
6
7
8
9
10
@Bean
public TaskExecutor executors () {
return new TaskExecutor () {
private static ExecutorService executorService = Executors . newVirtualThreadPerTaskExecutor ();
@Override
public void execute ( Runnable task ) {
executorService . execute ( task );
}
};
}
接下来写一个测试用的方法,并加上 @Async
注解:
1
2
3
4
5
@Async
public void asyncFunc () throws InterruptedException {
Thread . sleep ( 5000 );
log . info ( "async func finished" );
}
最后创建http端点作为入口:
1
2
3
4
5
6
7
@GetMapping ( "/test" )
public String test () throws InterruptedException {
log . info ( "start call" );
asyncTest . asyncFunc ();
log . info ( "end call" );
return "async func called" ;
}
按照经验,调用 /test 时,会立即得到响应: async func called
,而方法将在 5 秒后完成。启动程序调用一下试试:
1
2
3
11:04:19.704 INFO [ nio-8080-exec-2] -- start call
11:04:19.704 INFO [ nio-8080-exec-2] -- end call
11:04:24.720 INFO [ ] -- async func finished
结果符合预期。