環(huán)境:SpringBoot3.2.5 + JDK21
SpringBoot從3.2.0-M1版本開始支持虛擬線程。虛擬線程是JDK 21版本正式發(fā)布的一個(gè)新特性,它與平臺(tái)線程的主要區(qū)別在于虛擬線程在運(yùn)行周期內(nèi)不依賴操作系統(tǒng)線程,而是與硬件脫鉤,因此被稱為“虛擬”。這種解耦是由JVM提供的抽象層賦予的,使得虛擬線程的運(yùn)行成本遠(yuǎn)低于平臺(tái)線程,并且可以消耗更少的內(nèi)存。因此,從SpringBoot 3.2.0-M1開始,通過使用虛擬線程,提升系統(tǒng)的整體性能。
虛擬線程在項(xiàng)目中應(yīng)用時(shí)你稍不注意就可能出現(xiàn)問題。本篇文章將要講述的是在非Web應(yīng)用的情況下使用虛擬線程出現(xiàn)的問題(并非BUG)。
注意:本案例是非Web應(yīng)用。只要你不要引入spring-boot-starter-web模塊或者下面配置后都將以非web模式下運(yùn)行。
public static void main(String[] args) { new SpringApplicationBuilder() .sources(SpringbootNonWebApplication.class) // 即便引入了web模塊,但這里設(shè)置為非web應(yīng)用 .web(WebApplicationType.NONE) .run(args) ;}
非web應(yīng)用,啟動(dòng)容器后并不會(huì)啟動(dòng)嵌入式的web server,如果你當(dāng)前應(yīng)用中并沒有其它線程執(zhí)行(非守護(hù)線程),那么程序?qū)⒆詣?dòng)停止(啟動(dòng)即停止)。
圖片
啟動(dòng)完后自動(dòng)停止。
在一個(gè)非web環(huán)境下啟動(dòng)定時(shí)任務(wù):
@Componentpublic class TaskComponent { @Scheduled(fixedRate = 3000) public void task1() throws Exception { System.out.printf("當(dāng)前執(zhí)行線程: %s%n", Thread.currentThread()) ; // TODO 執(zhí)行任務(wù) TimeUnit.SECONDS.sleep(1) ; }}
上面定義了每隔3s執(zhí)行的定時(shí)任務(wù)(記得通過@EnableScheduling注解開啟任務(wù)調(diào)用功能)。
啟動(dòng)服務(wù)
圖片
程序規(guī)律的執(zhí)行,每隔3s輸出信息。
接下來開啟虛擬線程。
如果運(yùn)行的是 Java 21 或更高版本,可以通過配置如下屬性來啟用虛擬線程。
spring: threads: virtual: enabled: true
再次運(yùn)行程序
圖片
根據(jù)打印信息,執(zhí)行線程確實(shí)是通過虛擬線程執(zhí)行,但是僅僅啟動(dòng)時(shí)輸出了一條信息,程序就終止了,這肯定不是我們想要的。什么原因呢?
這是一段非常簡單的代碼了
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() ;
輸出結(jié)果:
start...1613150235234 over...1613150240238
程序等待3s后終止。接下來將上面Thread線程做如下配置:
// 設(shè)置為守護(hù)線程t.setDaemon(true) ;
再次執(zhí)行,這次執(zhí)行控制臺(tái)不會(huì)有任何的輸出程序就終止了。
在Java中當(dāng)所有非守護(hù)線程都執(zhí)行完以后,守護(hù)線程會(huì)自動(dòng)終止;守護(hù)線程一般用于執(zhí)行后臺(tái)任務(wù),資源清理等。
接下來通過虛擬線程執(zhí)行上面的代碼:
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后程序終止,只輸出如下結(jié)果:
start...1613840844449
虛擬線程難道也是守護(hù)線程?
通過如下代碼查看上面的虛擬線程是否是守護(hù)線程:
System.out.println(t.isDaemon()) ;
輸出結(jié)果:
true
既然是守護(hù)線程,那么程序自動(dòng)停止也就不意外了。下面是來自官方對虛擬線程與平臺(tái)線程的區(qū)別:
既然虛擬線程是守護(hù)線程,那么要如何解決上面的問題呢?在SpringBoot3.2.0-RC1版本開始為SpringApplication添加"keep-alive"屬性,專門解決虛擬線程問題。
可以通過如下配置開啟keepAlive。
spring: main: keep-alive: true
通過上面的配置后,再次運(yùn)行上面的程序
圖片
這時(shí)候程序不會(huì)退出了一直運(yùn)行。?
當(dāng)開啟上面的spring.main.keep-alive=true后,springboot在啟動(dòng)時(shí)會(huì)注冊一個(gè)監(jiān)聽器。
public class SpringApplication { public ConfigurableApplicationContext run(String... args) { // ... prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); // ... } private void prepareContext(...) { // ... // SpringBoot在啟動(dòng)時(shí)準(zhǔn)備Environment時(shí)會(huì)自動(dòng)將spring.main下的 // 屬性配置綁定到當(dāng)前的SpringApplication對象中(屬性)。 if (this.keepAlive) { // 添加事件監(jiān)聽 context.addApplicationListener(new KeepAlive()); } // ... }}
事件監(jiān)聽程序KeepAlive。
private static final class KeepAlive implements ApplicationListener<ApplicationContextEvent> { public void onApplicationEvent(ApplicationContextEvent event) { if (event instanceof ContextRefreshedEvent) { // Spring上下文刷新完成 startKeepAliveThread(); } // Spring容器在關(guān)閉時(shí) else if (event instanceof ContextClosedEvent) { stopKeepAliveThread(); } } private void startKeepAliveThread() { // 啟動(dòng)異步線程,一直休眠(保證一直運(yùn)行著,這樣程序就不會(huì)終止了) Thread thread = new Thread(() -> { while (true) { try { Thread.sleep(Long.MAX_VALUE); } } }); if (this.thread.compareAndSet(null, thread)) { // 非守護(hù)線程 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實(shí)現(xiàn)邏輯還是非常簡單的。
本文鏈接:http://www.tebozhan.com/showinfo-26-87991-0.htmlSpringBoot3使用虛擬線程一定要小心了
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
下一篇: Go語言整型(整數(shù)類型)的詳解