资讯详情

Java 21安装配置与Spring Boot 3.5虚拟线程实战

📅 2026/9/27 1:39:38 | 华诺云谱 👁 阅读
Java 21安装配置与Spring Boot 3.5虚拟线程实战
你在公司里把项目从 Java 8 升到 Java 17 还没多久Java 21 就已经成了不少团队的新目标。作为又一个长期支持版本它带来的不是挤牙膏式更新而是虚拟线程、分代 ZGC、switch 模式匹配这一批等了多年才转正的能力。这篇内容我打算从 JDK 21 的下载、安装、环境配置一直讲到和 Spring Boot 3.5 配合启用虚拟线程的实操路径适合正在选型升级、或者单纯想在自己机器上试试新特性的开发者。我会尽量把每个环节里容易踩的坑都提前标出来让你照着做就能顺利跑起来。1. 为什么 Java 21 值得关注以及如何选择发行版1.1 核心特性速览虚拟线程、ZGC、switch 模式匹配Java 21 是 Oracle 发布的长期支持版本默认支持到 2031 年 9 月维护周期非常充裕。它最吸引人的一点是虚拟线程终于转正了这直接改变了传统“一个请求一个线程”的并发模型。以前你用线程池撑高并发每个线程背后都对应一个操作系统线程线程多了上下文切换开销大内存占用也大所以大家习惯把线程池大小调成 CPU 核数的两倍或者固定几百个。虚拟线程则不一样它由 JVM 调度底层复用了少数平台线程启动和切换的成本低到可以忽略你可以放心地为每个任务都创建一个虚拟线程数量级能到十万甚至百万。除了虚拟线程分代 ZGC 也在这代转正。之前 ZGC 给人的印象是停顿极短但内存回收不如 G1 全面分代之后年轻代和老年代分开处理既能保持低停顿又减少了 Full GC 概率对大堆场景特别友好。再比如 switch 模式匹配和记录模式写业务代码时能明显少掉一坨类型转换和 if-else代码表达能力上升一个台阶。字符串模板还在预览阶段我不建议生产环境直接用但拿来体验一下新语法没问题。如果你只是想跑 Spring Boot 应用Java 21 配合 Spring Boot 3.2 以上版本就能开启虚拟线程到了 Spring Boot 3.5虚拟线程已经是非常稳定的默认推荐配置了。也就是说选择一个合适的 JDK 21 发行版是你所有后续操作的第一步。1.2 主流发行版对比Oracle JDK、Temurin、Liberica 怎么选下载 JDK 之前先明确一个概念JDK 不等于 Oracle JDK。市面上基于 OpenJDK 构建的发行版有很多它们核心功能几乎一致区别主要在授权方式、更新策略和带不带商业组件。我自己的建议优先级是这样发行版维护方授权特点适用场景Oracle JDKOracleNFTC 许可个人和低规模环境免费商业大规模需订阅企业生产环境有预算、需要官方支持Eclipse TemurinAdoptium 社区GPLv2 CE 例外完全免费我最推荐日常开发、小型生产都非常合适Amazon CorrettoAWS免费长期更新部署在 AWS 生态或需要特定补丁LibericaBellSoft免费版足够用想用 JavaFX 等额外组件时很顺手我日常开发用的是 Temurin原因很简单更新及时、安装包齐全、社区活跃遇到问题搜解决方案也容易。Oracle JDK 其实也没问题尤其你在 Windows 上用的是官方安装包自带更新机制适合不想折腾的人。不过如果公司有合规审查建议优先确认一下 NFTC 条款边界别把商业环境的使用方式搞模糊了。选发行版时还有一个容易忽略的点确认你需要的架构。大多数笔记本是 x86_64但 Apple Silicon 要选 AArch64 版本否则装上去会通过 Rosetta 转译性能打折。下载前多看一栏是否有 aarch64、x64 的标识。2. 下载前的准备与下载实操2.1 先弄清楚本地系统环境再选定安装包格式无论你用什么发行版第一步都是看清楚自己的操作系统类型。Windows 系统还需要区分是 64 位还是 32 位不过现在 32 位 JDK 基本没人用了大家统一用 x64 就行。macOS 要区分 Intel 和 Apple SiliconLinux 则要看用的是 deb 系还是 rpm 系或者你更喜欢通用的 tar.gz。包格式的选择直接影响安装路径。Windows 下有 .zip 和 .msi 两种常见格式zip 适合想自己控制目录、不用管理员权限的场景msi 则适合一键安装并自动配置环境变量。macOS 有 .tar.gz 和 .dmgLinux 有 .tar.gz、.deb 和 .rpm。如果你嫌麻烦macOS 还可以用 Homebrew一行 brew install openjdk21 就把环境配好了省心很多。Linux 若是 apt 系多数发行版仓库里自带 openjdk-21-jdk只是版本可能略旧。我在 Windows 上更倾向于下载 zip 包解压到 D:\Java\jdk-21然后手动配 JAVA_HOME。这样做的好处是换版本时只改环境变量一个值不用反复跑卸载安装程序。生产服务器上我一般用系统的包管理器安装比如 apt install因为之后打补丁方便不用自己盯着官网新版本。2.2 从官网下载对应版本的完整步骤打开 Adoptium 的官网大多能找到最新的 Temurin 21 版本在首页选择 Java 21、你的操作系统、架构和包类型然后点下载按钮即可。以 Temurin 的 GitHub Releases 页面为例你会看到一堆文件命名规则类似 OpenJDK21U-jdk_x64_windows_hotspot_21.0.x_x.zip其中 x64 表示架构windows 表示系统hotspot 表示用的是 HotSpot 虚拟机。下载时我建议优先选 .zip 或 .tar.gz 这种免安装格式原因前面说了便于手动控制。假如你选的是 Oracle JDK官网下载页会要求你先接受许可协议然后才能拿到下载链接。Oracle 的下载页还会区分 .dmg、.rpm、.deb、.zip注意别选错。下载文件之后千万别急着解压先做一件事校验 SHA256 值。官网每个文件旁都列有对应的 SHA256你可以用下面的命令核对确保文件在传输过程中没有被篡改或损坏Windows PowerShell 下执行Get-FileHash .\OpenJDK21U-jdk_x64_windows_hotspot_21.0.x_x.zip -Algorithm SHA256Linux 或 macOS 下执行echo 官网给的sha256值 文件名.zip | sha256sum -c -如果校验结果不一致那就重新下载基本可以断定文件不完整。这一步虽然啰嗦但可以省掉后面解压到一半报错、运行时莫名崩溃的折磨。提示不要从非官方镜像站、网盘或不明渠道下载 JDK。JDK 是基础运行环境一旦被人植入恶意代码你的所有 Java 应用都可能遭殃。认准 Adoptium、Oracle 官网、Windows/Linux 发行版官方源就好。2.3 免安装包与安装包的取舍建议免安装包和安装包各有利弊我实际用下来总结了几条经验。拿 Windows 来说msi 安装包最大的优势是自动写注册表、自动设置 JAVA_HOME 和 PATH还带默认的更新策略。缺点也很明显会关联系统服务、卸载时需要管理员权限而且默认装到 C:\Program Files\Java 这种带空格的路径某些老旧的构建脚本可能处理不好空格。zip 包就没有这些问题你想放哪儿就放哪儿删除时直接删目录即可。macOS 用户如果喜欢图形化操作dmg 安装包会自动帮我放到 /Library/Java/JavaVirtualMachines 目录下系统全局都能识别。Linux 上 .deb/.rpm 包最省心它由包管理器统一维护卸载升级都很干净。如果你需要同时测试多个 JDK 版本我强烈建议使用 SDKMAN 这类版本管理工具安装、切换一条命令搞定。比如 Linux/macOS 下sdk install java 21.0.1-tem sdk use java 21.0.1-tem这个工具实际上帮你管理的是各发行版的解压目录并动态调整 PATH 和 JAVA_HOME 的指向。Windows 上没有原生的 SDKMAN但可以用手动修改环境变量的方式模拟或者用比如 jEnv 的 Windows 替代品不过配置成本和稳定性都不如直接改 JAVA_HOME。3. 安装实操Windows、macOS、Linux 全流程3.1 Windows 环境变量配置与验证Windows 上我用 zip 包演示一遍完整流程。假设你已经把 zip 解压到 D:\Java\jdk-21接下来打开系统属性里的环境变量面板。右键“此电脑” → 属性 → 高级系统设置 → 环境变量。在用户变量中新增一条JAVA_HOMED:\Java\jdk-21然后编辑 Path 变量新增一行%JAVA_HOME%\bin这里有个非常容易踩的坑如果你之前装过其他 JDK 或 JREPath 里可能存在一条指向旧目录的绝对路径一定要把它删掉否则执行 java -version 时命中的可能还是旧版本。改完环境变量务必新开一个终端窗口再验证因为 CMD 和 PowerShell 的环境变量是从父进程继承的旧窗口不会刷新。验证命令有两步java -version javac -version如果输出里显示的是 openjdk 21.0.x说明安装成功。如果 java -version 正确但 javac 报错找不到命令那么多半是 Path 里只配了 JRE 路径没有 JDK 的 bin或者 JAVA_HOME 没指向 JDK 根目录而是指向了 JRE 目录。另外确认一下 java 命令实际路径避免被其他程序干扰where java3.2 macOS 安装细节Intel 与 Apple Silicon 的不同处理macOS 上如果你用 tar.gz 包下载后解压得到的是 jdk-21.jdk 目录把它移动到 /Library/Java/JavaVirtualMachines/ 目录下即可被系统识别sudo mv jdk-21.jdk /Library/Java/JavaVirtualMachines/然后执行 /usr/libexec/java_home -V 可以看到所有已安装的 JDK 版本。这类工具会输出一个 JAVA_HOME 路径你可以把它写进 shell 配置文件中echo export JAVA_HOME$(/usr/libexec/java_home -v 21) ~/.zshrc source ~/.zshrcApple Silicon 需要注意的点是下载时一定要选择 aarch64 或 arm64 版本如果误下了 x64 版本也能运行但 JIT 编译出来的代码要经过 Rosetta 转译性能损失大约在 10%~20% 之间。虚拟线程这种高并发场景下这种损失会被放大所以架构别选错。如果你选择用 Homebrew那么不需要这些手动步骤Homebrew会自动处理好路径和软链接问题。我在 mac 上多次遇到的一个问题是zsh 的 PATH 顺序里/usr/bin 下的旧 java 排在前面导致新装的 JDK 不生效。此时可以改 ~/.zshrc 里 export PATH/usr/local/opt/openjdk21/bin:$PATH 把它放到最前面即可。macOS 上还有一种常见的 JDK GUI 安装方式是 .dmg 文件双击按提示把 jdk-21.jdk 拖入目标目录就行本质上还是往 /Library/Java/JavaVirtualMachines 里复制。这个过程非常无脑不展开讲了。3.3 Linux 服务器安装tar.gz 与包管理器都可行Linux 服务器上我通常习惯用 tar.gz 安装到 /usr/local/java 目录因为这样不依赖发行版仓库里的版本可以自己指定 JDK 版本。示例流程sudo mkdir -p /usr/local/java sudo tar -zxvf OpenJDK21U-jdk_x64_linux_hotspot_21.0.x.tar.gz -C /usr/local/java解压后配置环境变量。系统级推荐创建一个文件避免污染 /etc/profileecho export JAVA_HOME/usr/local/java/jdk-21 | sudo tee /etc/profile.d/jdk21.sh echo export PATH$JAVA_HOME/bin:$PATH | sudo tee -a /etc/profile.d/jdk21.sh source /etc/profile.d/jdk21.sh如果你用了 .deb 或 .rpm 包包管理器会自动处理好这些然后只需要确认sudo update-alternatives --config java这个命令可以列出系统里所有 java 替代项切换到刚安装的版本。Linux 服务器如果通过 SSH 连接配置环境变量后要注意普通用户的 ~/.bashrc 若覆盖了 PATH可能导致全局配置不生效。此时在用户配置里再写一笔即可。还有一点经验容器环境里通常不需要真正“安装”JDK直接依赖 base 镜像比如 eclipse-temurin:21-jre 或 maven:3.9-eclipse-temurin-21反而更干净。有的镜像只带 JRE 不带 JDK如果你要在容器里编译就得选带 jdk 的镜像。3.4 验证安装的几个实用命令组合安装完之后我习惯跑一组命令确认环境健康而不只是看个版本号就完事java -version javac -version echo $JAVA_HOME which java还有一个容易被忽略的检查点编译一个简单的 HelloWorld 验证 Java 编译器和工作区没问题。public class Hello { public static void main(String[] args) { System.out.println(Java 21 works!); } }javac Hello.java java Hello输出 Java 21 works! 就说明从编译到运行整条链路正常。如果 javac 编译能过但 java 运行报错通常意味着运行环境只配了 JRE 没有配 JDK 的 bin或者存在多个版本冲突。注意在 Windows 上有些人会同时安装 JRE 8 和 JDK 21导致 java 命令运行的是 JRE 8而 javac 运行的是 JDK 21。这种组合混乱特别容易出现在老项目迁移场景中。解决方式是彻底删除旧 JRE或用 where java 定位命令实际路径确保所有 java 相关命令都来自同一个 JDK 根目录。4. 虚拟线程Java 21 的重头戏与 Spring Boot 3.5 启用配置4.1 虚拟线程原理简述为什么它能支撑高并发理解虚拟线程可以把它看成是 JVM 自己管理的“轻量级线程副本”它不直接对应操作系统线程而是由 JVM 调度到少量的载体线程上执行。传统模型里你创建一千个线程就有一千个内核线程映射内核调度器要花大量资源维护它们而虚拟线程的创建成本极低挂起和恢复也不需要内核干预只在发生阻塞时由 JVM 自动切换到其他虚拟线程继续跑。从代码层面看虚拟线程和普通线程的使用方式几乎没区别都可以用 Runnable 交给线程池也都可以用 CompletableFuture 异步执行。区别在于你用虚拟线程时不需要小心翼翼地设置线程池大小。就像餐厅临时工忙时多叫几个闲时自然解散不必强行维持一支固定规模的正式员工队伍。Java 21 里创建虚拟线程最直观的方式是Thread.builder().virtual().task(() - { // 业务逻辑 }).start();实际开发中我们很少直接用这么底层的 API更多是依托 Spring Boot 的容器自动配置。但知道这层原理对你调优非常有帮助。4.2 Spring Boot 3.5 开启虚拟线程的两个关键配置Spring Boot 从 3.2 开始就支持让 Tomcat 使用虚拟线程处理请求只需要在 application.properties 里加一行spring.threads.virtual.enabledtrueSpring Boot 3.5 对虚拟线程的支持更加成熟官方把虚拟线程作为默认的 web 应用并发方案之一你不需要写任何自定义的 Executor容器启动时自动创建一个使用虚拟线程的 TaskExecutor 来处理请求。这行配置会同时影响 Web 容器处理请求的线程模型以及 Spring 的 Async 异步任务。如果你用的不是 Spring Boot 自动装配而是自己管理线程池那么可以参考下面手动创建虚拟线程执行器的方式ExecutorService executor Executors.newVirtualThreadPerTaskExecutor();这个执行器的语义是“每个任务一个虚拟线程”你不会看到排队积压因为只要创建即可执行。这种方式天生的抗阻塞能力非常适合 IO 密集型的服务比如访问数据库、调用外部 API 等场景。但要注意虚拟线程对 CPU 密集型的计算任务帮助有限。你如果跑了大量死循环似的计算逻辑虚拟线程并不会比平台线程更快它可能因为频繁让出 CPU 而增加调度开销。4.3 一个完整的 Spring Boot 3.5 Java 21 高并发示例下面我给一个可以直接跑起来的小型演示项目。环境要求JDK 21、Spring Boot 3.5、Maven 3.9。pom.xml 核心依赖只有 spring-boot-starter-webparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.0/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies然后在 application.properties 里开启虚拟线程spring.threads.virtual.enabledtrue接着写一个模拟 IO 阻塞的接口RestController public class DemoController { GetMapping(/io) public String ioTask() throws InterruptedException { TimeUnit.MILLISECONDS.sleep(200); return done; } GetMapping(/health) public String health() { return ok; } }启动应用后用压测工具向 /io 发送大量并发请求会发现即使平台线程数极少请求依然能全部被及时处理。这是因为每个请求都运行在独立的虚拟线程上sleep 阻塞时虚拟线程挂起让出底层平台线程给其他请求使用。如果你用传统 Tomcat 默认线程池200 个请求就能轻易把所有线程阻塞住后续请求必须排队等待。换成虚拟线程后阻塞不再成为瓶颈吞吐量能提升一个数量级。这就是虚拟线程最直观的价值。我在实际项目里还发现一个点开启虚拟线程后CompletableFuture 里嵌套调用外部接口时默认使用的线程池并不受影响它仍会用 ForkJoinPool.commonPool 里的有限线程。想全链路都用虚拟线程就得为这些异步操作指定统一的虚拟线程执行器。如果用了 Spring 的 AsyncTaskExecutor则可以写一个配置类注入新的执行器Configuration public class AsyncConfig { Bean(virtualTaskExecutor) public TaskExecutor virtualTaskExecutor() { return new VirtualThreadTaskExecutor(); } }不过 Spring Boot 3.5 里虚拟机线程的自动配置已经覆盖了大多数开箱即用的场景没特殊需求不要重复造轮子。提示虚拟线程虽好但不要把传统线程池里“连接复用”的思维生搬硬套。比如数据库连接这种有限资源你依然要控制连接池大小不可能因为虚拟线程无限多就让连接池也无限大。虚拟线程解决的是“调度的开销”不解决“资源的争用”。5. 常见问题排查与避坑指南5.1 安装和运行阶段最高频的报错整理我把多年来帮同事处理的 JDK 安装问题整理成一张表按影响面排序现象可能原因解决思路java -version 显示旧版本PATH 顺序不对用 where java 查看实际路径把新 JDK 的 bin 调到前面javac 命令找不到只装了 JRE 或 JAVA_HOME 配置错检查 JAVA_HOME 是否指向 JDK 根目录Spring Boot 启动报 Unsupported class file major version 65Maven 或 IDE 用的是旧 JDK确认 mvn -v 和 IDE 使用的 JDK 都是 21虚拟线程配置不生效用了 Spring Boot 3.1 或更低版本升级到 3.2最好是 3.5mac 上 java 命令指向旧的 /usr/bin/javaPATH 顺序或 java_home 工具配置 JAVA_HOME 后将其 bin 放在 PATH 最前Linux 解压后无法执行 java 命令未配置 PATH 或权限问题检查执行权限chmod x 或重新解压Windows 下 Maven 编译使用错误 JVMJAVA_HOME 指向了旧的 JDK修改 JAVA_HOME 并重启终端/IDE这个表格里最容易被忽视的其实是“IDE 自带的 JDK 与命令行不一致”。IDEA 和 Eclipse 都有自己的 JRE 配置你命令行升级了 JDK但 IDE 里的编译目标还是旧版本导致代码明明用了 Java 21 的新语法却在 IDE 里报红。解决方法是到 IDE 的项目结构设置里把 Project SDK 和 Project language level 都改成 21。5.2 四个我亲身踩过的坑以及对应的处理方案第一个坑是 Windows 下环境变量修改后不生效。很多人改完 PATH 后直接继续用原来的 CMD 窗口敲 java -version发现还是旧版本就以为配置失败了。其实 Windows 的终端从父进程继承了环境变量旧窗口不会刷新必须新开一个窗口。另外如果用的是 PowerShell可以通过 [Environment]::SetEnvironmentVariable 这个方式临时改当前进程不过最稳妥的还是新开会话。第二个坑是 Linux 下装了 openjdk-21-jdk 之后Java 命令却指向了系统自带的 OpenJDK 11。这是因为 Debian 系的 update-alternatives 有默认优先级逻辑在同时存在多个 JDK 时系统不会自动切到新版。你得显式执行 sudo update-alternatives --config java 来切换注意 javac 也要单独切换。第三个坑是 Spring Boot 3.5 开启虚拟线程后日志里看不到虚拟线程名导致排查问题分不清具体是哪个请求的事务。这个问题看似小但生产环境里很麻烦。解决办法是在配置文件里把 spring.task.execution.thread-name-prefix 设为 virtual-thread-或者直接用 Thread.ofVirtual().name(custom-, 0) 创建有辨识度的虚拟线程。虽然这只是运维细节影响排查效率。第四个坑是虚拟线程 ThreadLocal 的组合拳打歪了。Java 21 的虚拟线程支持 ThreadLocal但虚拟线程数量多、生命周期短大量使用 ThreadLocal 会显著增加内存占用。以前你为了省资源用线程池加 ThreadLocal 缓存一些上下文信息这个模式到虚拟线程时代就不合适了。官方更推荐使用 ScopedValueJava 21 预览它还不太稳定所以生产上尽量少用 ThreadLocal或者把数据显式传给方法。注意不要把虚拟线程套进固定线程池里。Executors.newFixedThreadPool(10) 里跑虚拟线程任务是反模式虚拟线程的价值就是按需创建你给它们一个上限反而失去了意义。真要限制并发可以使用信号量 Semaphore 而不是池化虚拟线程。5.3 多版本 JDK 共存的管理技巧很多人的机器上同时有 Java 8、17、21切换起来最容易出错。我的习惯是永远不直接修改系统 PATH 里的 java 全局路径而是统一维护几个版本的安装目录然后用脚本或工具切换。在 Linux 和 macOS 上SDKMAN 是我的首选。它支持同时安装多个发行版和多个 JDK 版本sdk list java 可以查看所有可用版本sdk install java 21.0.1-tem 安装sdk use java 21.0.1-tem 临时切换sdk default java 21.0.1-tem 设置默认版本。我个人的经验是用 sdk default 而不是 sdk use因为如果你在某个 shell 会话里用了 sdk use新开的终端又会回落到默认版本容易造成“时好时坏”的诡异现状。Windows 下我用一个更简单的方法创建两个批处理脚本 switch_to_jdk17.bat 和 switch_to_jdk21.bat内部动态设置 JAVA_HOME 和 PATH一键切换。脚本内容大致是这样echo off setx JAVA_HOME D:\Java\jdk-21 set PATH%JAVA_HOME%\bin;%PATH% java -versionsetx 命令会持久写入用户环境变量注意最好只对当前用户生效不要改系统级的环境变量避免影响其他软件。输入法、数据库客户端等软件可能会有自己的 JVM 配置它们不会完全跟随系统 JAVA_HOME这种软件一般自带配置文件如 MySQL Workbench 和 DBeaver。如果升级 JDK 后发现这类工具连不上优先去应用的配置里指定新的 JDK 路径。5.4 从 Java 17 迁移到 21 时常用的几个兼容性处理Java 17 到 21 大体上是兼容的但有几个点需要特别留意。首先是强封装 JDK 内部 API 的推进Java 17 用 --illegal-accessdeny 已经比较严格Java 21 更倾向于不允许反射访问 JDK 内部类。如果你引用了 sun.misc.Unsafe 或 com.sun.org.apache.xerces 这样的包很可能会在启动时收到 IllegalAccessError。解决办法是找替代库或者加上 --add-opens 参数临时开放相关模块。其次Java 21 里废弃了 finalize 方法同时在顺带清理一些老旧 API。如果你的项目还在重写 finalize这段代码需要尽快替换为 Cleaner 或者 AutoCloseable。Spring Boot 3.5 本身已经适配了 Java 21所以从 17 升到 21 时主要风险集中在老代码内部的直接依赖。再一个容易被忽略的是字节码版本问题。Java 21 编译出的 class 文件是 major version 65如果你的运行容器被锁定在 Java 17 或更低版本就会报 UnsupportedClassVersionError。这在升级过程中特别常见比如 Maven 编译用的 JDK 21但 Tomcat 容器跑的还是 JDK 17。建议升级时统一编译环境和运行环境的 JDK 版本以免陷入这种两头对不上的状态。排查这些兼容性问题时jlink 是一个很有用的工具可以生成一个只包含所需模块的定制运行时。它既能减小镜像体积也能强制暴露模块依赖关系快速定位哪些代码引用了不该引用的内部 API。Java 21 的模块化体系依旧完善和 jlink 配套使用没有违和感。6. 实际项目落地时的几个选型建议6.1 虚拟线程能带来多大收益先评估场景再动手网上关于虚拟线程的测试报告很多动不动就是每秒几万请求的截图。落到你的项目里先判断服务是不是 IO 密集型。如果接口主要在做数据库查询、外部 API 调用、消息队列读写那虚拟线程几乎能无损地提升吞吐量。如果是计算密集型那虚拟线程优势不大更值得关注的是分代 ZGC 或者并行 GC 参数。我之前接手过一个网关服务压测时发现 80% 的耗时都卡在外部认证服务上tomcat 线程全部阻塞。用 Java 21 Spring Boot 3.5 开启虚拟线程后单机吞吐量从 1200 QPS 涨到 4600 QPS关键是代码一行没改只改了配置。这种结果很有说服力。反例也有我之前试过一个纯计算转码的服务开启虚拟线程后反而出现了轻微的性能回退后来干脆关掉虚拟线程保持默认平台线程池。所以建议你先做个五分钟的小实验写个模拟阻塞的 Controller压测对比开启前后的指标。如果提升明显就全面铺开如果提升不大也不必强上。6.2 升级 JDK 21 的团队协作注意事项升级 JDK 不只是个人开发环境的事它会涉及 CI/CD 流水线、测试环境、Docker 镜像等多个环节。我的建议是按这个顺序推进先在 CI 里把 Maven/Gradle 的 JDK 版本指到 21跑一遍完整测试然后把测试环境的 JDK 升级最后再动生产环境。团队里经常发生的情况是代码里用到了 Java 21 新特性但 CI 节点上还是 JDK 17结果 mvn compile 一遍过mvn test 却报类似“无法解析符号 getExecutable”之类的错误。这是因为新 API 在旧 JDK 上编译直接失败。提前在 pom.xml 里配置 maven.compiler.release21可以明确要求编译器使用 21 的 API而不是当前 JDK 版本能访问的 API。Docker 镜像里也需要同步调整。如果你是部署在容器里要注意基础镜像的 Java 版本需要和编译环境一致否则镜像构建时使用 JDK 21 编译运行时用 JRE 17 会直接出错。最稳妥的方式是让构建阶段和运行阶段用同一个版本的 eclipse-temurin 镜像。6.3 是否需要升级到 Spring Boot 3.5成本如何评估Spring Boot 3.5 不是让你必须升级的版本但它确实非常值得关注。它的老版本 3.2 和 3.3 虽然也支持虚拟线程但 3.5 在虚拟线程任务的生命周期管理、嵌入式 Tomcat 配合等方面修得更完整而且 3.5 已经适配了 Java 21 LTS 的特性像 RestClient 这类新 API 也能用得顺。如果你的项目还在 Spring Boot 2.7 或更低版本升级到 3.5 的跨度会比较大涉及 Jakarta EE 包名替换、Spring Security 配置结构调整等。这时候就不要把 JDK 升级和 Spring Boot 升级混在一起做要分步先升 Spring Boot 到 3.x保证在 JDK 17 上稳定运行再升 JDK 到 21。这样每一步的回归范围更小排查问题也容易定位。如果项目比较新直接在 Spring Boot 3.5 JDK 21 上开发是最顺畅的路径。我个人的判断是未来两年内大部分 Java 服务都会迁到这个组合早点踩坑比晚点被迫迁移要好得多。最后再分享一点个人体会我最初是从 Java 8 直接跳到 21 的刚开始特别不适应项目结构里少了一堆 getter/setter也不太敢大量使用 record 和 switch 模式匹配。但真正跑起来之后发现代码简洁性是实打实的。尤其是把虚拟线程打开以后原来辛辛苦苦调的线程池参数直接删掉系统反而更稳定了。另外下载安装 JDK 这件事看着简单实际上很多问题的根源都在于环境变量。如果你在工作中遇到 java -version 输出和预期不符先把 where java、echo $JAVA_HOME、mvn -v 三连跑一遍多数情况下问题马上水落石出。配合 SDKMAN 或者 Windows 环境下脚本切换多个 JDK 版本就能避免手工修 Path 修到怀疑人生。希望这篇内容能帮你顺利把 Java 21 跑起来。如果你在安装或迁移过程中碰到特别奇怪的环境问题可以顺着这篇文章里的排查思路一一看下来大概率能找到症结所在。虚拟线程那条路值得走走稳了收益非常可观。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑