SpringBoot3使用虚拟线程一定要小心了

2024年 5月 10日 45.9k 0

环境: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,如果你当前应用中并没有其它线程执行(非守护线程),那么程序将自动停止(启动即停止)。

SpringBoot3使用虚拟线程一定要小心了-1图片

启动完后自动停止。

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注解开启任务调用功能)。

启动服务

SpringBoot3使用虚拟线程一定要小心了-2图片

程序规律的执行,每隔3s输出信息。

2.2 虚拟线程执行任务

接下来开启虚拟线程

如果运行的是 Java 21 或更高版本,可以通过配置如下属性来启用虚拟线程。

spring:
  threads:
    virtual:
      enabled: true

再次运行程序

SpringBoot3使用虚拟线程一定要小心了-3图片

根据打印信息,执行线程确实是通过虚拟线程执行,但是仅仅启动时输出了一条信息,程序就终止了,这肯定不是我们想要的。什么原因呢?

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

通过上面的配置后,再次运行上面的程序

SpringBoot3使用虚拟线程一定要小心了-4图片

这时候程序不会退出了一直运行。✔

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 {
  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实现逻辑还是非常简单的。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论