Spring Boot 3.2 中开箱即用的虚拟线程和 GraalVM

Spring Boot 3.2 前几天 发布 了,让我们用 Java 21、GraalVM 和 Virtual Threads(虚拟线程)来快速体验一下。

Spring Boot 3.2 支持:

  • Java 21
  • 虚拟线程
  • 原生镜像(自 2022 年 11 月 Spring Boot 3.0 发布以来,Spring Boot 已在生产中支持 GraalVM 原生镜像)

Java 21

2023 年 9 月 19 日 Java 21 发布。

正如宣布的那样,Java 21 在性能、稳定性和安全性方面进行了数千项改进,包括平台增强功能,这有助于开发人员提高生产力,推动整个组织的创新和发展。

Project Loom

其中一个比较重要的更新是虚拟线程(Virtual Thread),这是 Project Loom 提供的功能。具体细节这里就不多说了,你可以参考官方的 JEP:https://openjdk.org/jeps/444

GraalVM 和 Native image

GraalVM 是一款高性能 JDK,可使用另一种即时(JIT)编译器提高 Java 和基于 JVM 的应用程序的性能。

原生镜像(Native image)是一种将 Java 代码提前编译成独立可执行文件(称为原生镜像)的技术。该可执行文件包括应用程序类、其依赖项中的类、运行时库类以及 JDK 中静态链接的本地代码。

它不在 Java 虚拟机上运行,而是在不同的运行时系统中包含内存管理、线程调度等必要组件。与 JVM 相比,这样生成的程序启动时间更快,运行时内存开销更低。

https://www.graalvm.org/22.0/reference-manual/native-image/

快速尝试

首先安装 Java 21.0.1 graal,最简单的方法是使用 SDKMAN

sdk install java 21.0.1-graal

并将其指定为机器的默认 Java 版本:

sdk default java 21.0.1-graal

另一种安装方法是手动下载,安装。请参阅:https://www.graalvm.org/downloads/

使用 Spring Initializr 创建一个新的 Spring Boot 项目,并选择 Spring Boot 3.2.0、Java 21、Gradle - Groovy 以及 Spring WebGraalVM Native Support 依赖。

要在 Spring Boot 3.2 中启用虚拟线程,只需在 application.ymlapplication.properties 文件中设置一个属性:

spring.threads.virtual.enabled: true

添加如上配置后:

  • Tomcat 将使用虚拟线程处理 HTTP 请求。这意味着处理 Web 请求的代码(如 Controller 中的方法)将在虚拟线程上运行。
  • 在调用 @Async 方法时,Spring MVC 的异步请求处理和 Spring WebFlux 的阻塞执行支持现在将使用虚拟线程
  • 注解了 @Scheduled 的方法将在虚拟线程上运行

此外,一些特定的集成(整合)也会在虚拟线程上运行,如 RabbitMQ/Kafka 监听器和 Spring Data Redis/Apache pulsar 相关集成。但这些不在本文的讨论范围之内。

Controller

创建一个简单的 Controller 来处理 Tomcat 传入的 Http 请求:

@RestController
@RequestMapping("/test")
public class TestController {
    private static final Logger log = LoggerFactory.getLogger(TestController.class);

    @GetMapping
    public void test() {
        log.info("Rest controller method has been called {}", Thread.currentThread());
    }
}

异步任务

定义一个 @Async 异步任务方法,在应用启动时调用其 run 方法:

@Component
public class AsyncTaskExecutorService {
    private static final Logger log = LoggerFactory.getLogger(AsyncTaskExecutorService.class);

    @Async
    public void run() {
        log.info("Async task method has been called {}", Thread.currentThread());
    }
}

定时任务

定义一个 @Scheduled 方法,每 15 秒调用一次 run 方法:

@Component
public class SchedulerService {
    private static final Logger log = LoggerFactory.getLogger(SchedulerService.class);

    @Scheduled(fixedDelayString = "15000")
    public void run() {
        log.info("Scheduled method has been called {}", Thread.currentThread());
    }
}

测试

运行应用:

./gradlew bootRun

调用 Controller 端点:

curl — location — request GET 'localhost:8085/test'

响应如下:

Starting AppApplication using Java 21.0.1 with PID 38126
Started AppApplication in 1.131 seconds (process running for 1.491)
Async task method has been called VirtualThread[#52,task-1]/runnable@ForkJoinPool-1-worker-5
Scheduled method has been called VirtualThread[#46,scheduling-1]/runnable@ForkJoinPool-1-worker-1
Rest controller method has been called VirtualThread[#62,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-1
Scheduled method has been called VirtualThread[#46,scheduling-1]/runnable@ForkJoinPool-1-worker-1

如你所见,Controller、异步任务、定时任务方法的日志显示当前的线程为 ForkJoinPool 线程池中的线程。

根据 JEP 的解释,这是预期行为:

JDK 的虚拟线程调度器是一个工作窃取 ForkJoinPool,以先进先出(FIFO )模式运行。调度器的并行性(parallelism)是指可用来调度虚拟线程的平台线程数。

现在,尝试在 GraalVM 上运行它。

首先,需要构建一个 GraalVM 原生镜像:

# 该命令可能会比较耗时,请耐心等待
./gradlew nativeCompile

然后执行:

# app 是你的应用程序名
./build/native/nativeComplie/app

它也能运行,而且启动时间更快,这符合所宣称的 “与 JVM 相比,所生成的程序启动时间更快,运行时内存开销更低”。

本文中的完整示例代码,你可以在 这里 找到。

总结

使用虚拟线程的原生镜像允许我们编写的代码在性能和可扩展性方面达到与 Go 等类似的水平,同时保持 JVM 的强大生态系统。

不过,你必须考虑到,并非所有的库都已经采用了虚拟线程(在大多数情况下,需要将 ReentrantLock 代替 synchronize ),并且应该谨慎对待虚拟线程的工作逻辑。


Ref::https://medium.com/@egorponomarev/spring-boot-3-2-with-virtual-threads-and-graalvm-out-of-the-box-1911d3ebf0b6