SpringBoot3使用虛擬線程一定要小心了
環境:SpringBoot3.2.5 + JDK21
1.簡介
SpringBoot從3.2.0-M1版本開始支持虛擬線程。虛擬線程是JDK 21版本正式發布的一個新特性,它與平臺線程的主要區別在于虛擬線程在運行周期內不依賴操作系統線程,而是與硬件脫鉤,因此被稱為“虛擬”。這種解耦是由JVM提供的抽象層賦予的,使得虛擬線程的運行成本遠低于平臺線程,并且可以消耗更少的內存。因此,從SpringBoot 3.2.0-M1開始,通過使用虛擬線程,提升系統的整體性能。
虛擬線程在項目中應用時你稍不注意就可能出現問題。本篇文章將要講述的是在非Web應用的情況下使用虛擬線程出現的問題(并非BUG)。
2. 實戰案例
注意:本案例是非Web應用。只要你不要引入spring-boot-starter-web模塊或者下面配置后都將以非web模式下運行。
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(SpringbootNonWebApplication.class)
// 即便引入了web模塊,但這里設置為非web應用
.web(WebApplicationType.NONE)
.run(args) ;
}
非web應用,啟動容器后并不會啟動嵌入式的web server,如果你當前應用中并沒有其它線程執行(非守護線程),那么程序將自動停止(啟動即停止)。
圖片
啟動完后自動停止。
2.1 啟動定時任務
在一個非web環境下啟動定時任務:
@Component
public class TaskComponent {
@Scheduled(fixedRate = 3000)
public void task1() throws Exception {
System.out.printf("當前執行線程: %s%n", Thread.currentThread()) ;
// TODO 執行任務
TimeUnit.SECONDS.sleep(1) ;
}
}
上面定義了每隔3s執行的定時任務(記得通過@EnableScheduling注解開啟任務調用功能)。
啟動服務
圖片
程序規律的執行,每隔3s輸出信息。
2.2 虛擬線程執行任務
接下來開啟虛擬線程。
如果運行的是 Java 21 或更高版本,可以通過配置如下屬性來啟用虛擬線程。
spring:
threads:
virtual:
enabled: true
再次運行程序
圖片
根據打印信息,執行線程確實是通過虛擬線程執行,但是僅僅啟動時輸出了一條信息,程序就終止了,這肯定不是我們想要的。什么原因呢?
2.3 守護線程
這是一段非常簡單的代碼了
Thread t = new Thread(() -> {
try {
System.out.println("start..." + System.currentTimeMillis()) ;
TimeUnit.SECONDS.sleep(5) ;
} catch (Exception e) {
e.printStackTrace() ;
}
System.out.println(" over..." + System.currentTimeMillis()) ;
}) ;
t.start() ;
輸出結果:
start...1613150235234
over...1613150240238
程序等待3s后終止。接下來將上面Thread線程做如下配置:
// 設置為守護線程
t.setDaemon(true) ;
再次執行,這次執行控制臺不會有任何的輸出程序就終止了。
在Java中當所有非守護線程都執行完以后,守護線程會自動終止;守護線程一般用于執行后臺任務,資源清理等。
接下來通過虛擬線程執行上面的代碼:
OfVirtual virtual = Thread.ofVirtual().name("Pack-") ;
Thread t = virtual.start(() -> {
try {
System.out.println("start..." + System.currentTimeMillis()) ;
TimeUnit.SECONDS.sleep(5) ;
} catch (Exception e) {
e.printStackTrace() ;
}
System.out.println("over..." + System.currentTimeMillis()) ;
}) ;
TimeUnit.SECONDS.sleep(1) ;
等待1s后程序終止,只輸出如下結果:
start...1613840844449
虛擬線程難道也是守護線程?
通過如下代碼查看上面的虛擬線程是否是守護線程:
System.out.println(t.isDaemon()) ;
輸出結果:
true
既然是守護線程,那么程序自動停止也就不意外了。下面是來自官方對虛擬線程與平臺線程的區別:
- 虛擬線程始終是守護線程。Thread.setDaemon(boolean) 方法無法將虛擬線程更改為非守護線程。
- 虛擬線程的固定優先級為 Thread.NORM_PRIORITY。Thread.setPriority(int) 方法對虛擬線程不起作用。這一限制可能會在未來的版本中重新考慮。
- 虛擬線程不是線程組的活動成員。在虛擬線程上調用 Thread.getThreadGroup() 時,會返回一個名稱為 "VirtualThreads "的占位線程組。Thread.Builder API 沒有定義設置虛擬線程線程組的方法。
2.4 KeepAlive虛擬線程
既然虛擬線程是守護線程,那么要如何解決上面的問題呢?在SpringBoot3.2.0-RC1版本開始為SpringApplication添加"keep-alive"屬性,專門解決虛擬線程問題。
可以通過如下配置開啟keepAlive。
spring:
main:
keep-alive: true
通過上面的配置后,再次運行上面的程序
圖片
這時候程序不會退出了一直運行。?
2.5 實現原理
當開啟上面的spring.main.keep-alive=true后,springboot在啟動時會注冊一個監聽器。
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
// ...
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// ...
}
private void prepareContext(...) {
// ...
// SpringBoot在啟動時準備Environment時會自動將spring.main下的
// 屬性配置綁定到當前的SpringApplication對象中(屬性)。
if (this.keepAlive) {
// 添加事件監聽
context.addApplicationListener(new KeepAlive());
}
// ...
}
}
事件監聽程序KeepAlive。
private static final class KeepAlive implements ApplicationListener<ApplicationContextEvent> {
public void onApplicationEvent(ApplicationContextEvent event) {
if (event instanceof ContextRefreshedEvent) {
// Spring上下文刷新完成
startKeepAliveThread();
}
// Spring容器在關閉時
else if (event instanceof ContextClosedEvent) {
stopKeepAliveThread();
}
}
private void startKeepAliveThread() {
// 啟動異步線程,一直休眠(保證一直運行著,這樣程序就不會終止了)
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
}
}
});
if (this.thread.compareAndSet(null, thread)) {
// 非守護線程
thread.setDaemon(false);
thread.setName("keep-alive");
thread.start();
}
}
private void stopKeepAliveThread() {
Thread thread = this.thread.getAndSet(null);
if (thread == null) {
return;
}
// 終止線程
thread.interrupt();
}
}
SpringBoot實現邏輯還是非常簡單的。