ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从 JDK 21 安装到 Spring Boot 虚拟线程:完整上手实践

从 JDK 21 安装到 Spring Boot 虚拟线程:完整上手实践 Java 21 正式发布到现在已经两年多按理说早该是生产环境的默认选项可我发现身边还有大量项目停在 Java 8 和 Java 11。问起来就是一句话“怕升级麻烦。”但实际操作一遍之后我的结论是Java 21 的下载与安装难度真不高真正花时间的是搞清楚发行版怎么挑、环境变量怎么配、以及升级之后那把“虚拟线程”的刀怎么用好。这篇文章就把我从下载到安装、从验证到在 Spring Boot 3.5 里启用虚拟线程的完整过程写出来新手可以照着一步一步做老手也能直接抄配置跳过踩坑环节。1. 为什么我建议直接把 JDK 换成 211.1 “LTS”不是白叫的Java 的版本发布节奏是“功能版本六个月一个LTS 版本两三年一个”。11 是 2018 年、17 是 2021 年、21 是 2023 年 9 月正式发布。所谓 LTS意思是这个版本会进入一个很长的维护期而不是像 22、23、24 这种小版本一样“用完即弃”。对团队来说选 JDK 不是追新是选一个足够久的“安全稳定窗口”。Java 21 作为 LTS 版本意味着你把它装进生产环境后不用像功能版本那样每隔半年又面临一次“要不要升”的拷问。它的免费公开更新周期足以覆盖一次完整的技术债清理周期企业如果需要更长保障也还能从厂商那边购买扩展支持。所以从时间线上看21 就是 17 之后最值得落地的那个版本。1.2 Java 21 核心新特性速览Java 21 这一版的特性密度相当高挑几个对日常开发影响最大的说虚拟线程JEP 444这是最重要的一项虚拟线程从预览转正正式成为生产可用的功能。它解决了“线程太贵、并发受限”的老大难问题也是我后面单独拿一整章来讲的原因。有序集合 Sequenced CollectionsJEP 431List.getFirst()、getLast()、reversed()这些方法终于有统一接口了写代码时不用再自己判断下标。Record PatternsJEP 440和 switch 模式匹配JEP 441解构 record 类型、写类型分支时可以少写一堆if (obj instanceof Xxx)和强转的样板代码。分代 ZGCJEP 439新一代垃圾回收器默认开启分代模式GC 停顿更稳对大堆服务比较友好。String TemplatesJEP 430字符串内嵌表达式的预览功能写日志和拼接 SQL 模板时会爽很多但还在预览阶段别急着上生产。Structured ConcurrencyJEP 453和 Scoped ValuesJEP 446并发编程的更高层抽象目前是预览特性适合尝鲜。简单说Java 21 不是那种“只修修补补”的版本它把并发模型、语言解构能力、GC 都往前推了一大步。1.3 和 11、17 比这次升级值在哪里版本发布时间是否 LTS标志性能力什么时候选它Java 82014LTSLambda、Stream老系统兼容基线能迁就迁Java 112018LTS模块化、ZGC 初期、HttpClient中间过渡版Java 172021LTS密封类、长期免费授权目前很多团队的存量版本Java 212023LTS虚拟线程、Record Pattern、分代 ZGC新项目首选老项目列入升级规划很多团队卡在 17 不动的理由是“项目还能跑”。但问题在于新特性不会倒灌到旧版本里你等 Spring 生态的大版本把基线抬高之后再动成本只会更高。新项目我基本无脑推荐 21老项目就算这季度没排期也建议先把“升级 Java 21”这个技术债挂上号别等框架版本不兼容了才被动升级。2. 下载之前的三个关键选择2.1 选发行版别再用“OpenJDK 官网”来解决一切不少人搜索“Java 21 下载”第一个进的就是 Oracle 官网直接下 Oracle JDK。这当然能用但你要清楚不同发行版背后的授权和维护逻辑。核心语言和 API 大家都一样区别在于发布节奏、授权条款、以及补丁谁来打。发行版维护方适合场景注意点Oracle JDKOracle需要官方商业支持注意 OTN 授权条款商用要看清边界Eclipse TemurinEclipse Adoptium 社区绝大多数团队首选完全开源免费GPLv2CEAmazon Corretto亚马逊AWS 生态为主免费长期支持Azul ZuluAzul多平台覆盖需求强免费版够用Microsoft OpenJDK微软Azure 生态为主免费我个人对普通团队的建议是优先选 Eclipse Temurin它等价于社区维护的 OpenJDK 构建版免费、活跃、厂商背书也足够。下载入口是 adoptium.net 的 releases 页面按version21过滤即可。如果确实需要 Oracle 的官方支持再考虑 Oracle JDK。服务器上跑的发行版最好全团队统一别出现开发用 Temurin、测试机用 Corretto、生产用 Oracle 的混乱局面。2.2 选版本号与架构版本号里的门道版本号要看清21.0.6这种写法的意思是“大版本 21 补丁版本 0.6”。下载时永远选当前最新的补丁版本因为补丁版本里带着的是安全修复不是新功能没理由用旧的。架构更关键。Windows 上绝大多数人是 x64macOS 要区分 Intel 还是 Apple SiliconApple Silicon 必须选aarch64Linux 服务器则要看清是 x64 还是 ARM。JDK 的 jar 包是跨平台的但安装包里的 JVM 动态库和可执行文件都是和操作系统强绑定的下载错架构的结果就是安装完一跑就报错。下载文件命名里通常会带x64、aarch64、windows、linux、macos这类关键字别不看就直接点。2.3 选安装方式安装包、压缩包还是包管理器三种方式没有绝对好坏看你的使用场景Windows首选 MSI 安装包安装界面可以直接勾选“Add to PATH”“Set JAVA_HOME”省掉手动配环境变量的麻烦。zip 压缩包则适合想完全手动控制的场景。macOS官方 pkg 安装包和 Homebrew 都行。pkg 装完系统能自动识别Homebrew 则方便以后统一升级管理。Linux能用系统包管理器apt/yum/dnf就尽量用补丁跟着系统更新走需要固定目录隔离或者多版本共存时再用 tar.gz 解压到/opt下。不管哪种方式本质都是同一个动作把 JDK 放到一个固定目录然后让操作系统和开发工具知道这个目录在哪。这个“告诉别人位置”的动作就是下文要讲的 JAVA_HOME 和 PATH。3. Windows、macOS、Linux 的安装实操3.1 WindowsMSI 安装与 PATH 补课在 Adoptium 下载页选择x64的 Windows MSI 安装包双击一路 Next。如果安装界面有 “Set JAVA_HOME” 和 “Add to PATH” 的勾选项一定记得勾上。装完一定要新开一个 CMD 或 PowerShell 窗口再执行java -version旧窗口不会刷新环境变量这是新手最容易懵的地方。如果你没勾自动配置或者事后想手动确认按这个顺序来Win R输入sysdm.cpl打开系统属性进“环境变量”。在系统变量里新建JAVA_HOME值填 JDK 根目录比如C:\Program Files\Eclipse Adoptium\jdk-21.0.6.7-hotspot。找到系统变量里的Path新建一行%JAVA_HOME%\bin。全部点确定后重新打开一个终端窗口验证。这里有个高频错误有人把 JAVA_HOME 直接写成了...\jdk-21\bin这是不对的。JAVA_HOME 永远指向 JDK 根目录bin是追加在Path里用的。3.2 macOSpkg 与 Homebrew 双路线macOS 最简单的方式就是下载 pkg 安装包双击安装。装完打开终端执行/usr/libexec/java_home -V这个命令会列出系统里所有 JDK如果能看到 21 开头的路径就说明安装成功了。然后在~/.zshrc里加上export JAVA_HOME$(/usr/libexec/java_home -v 21) export PATH$JAVA_HOME/bin:$PATH用 Homebrew 的话更省事brew install openjdk21装完 brew 会提示你建一个软链接到/Library/Java/JavaVirtualMachines/按提示执行即可。这一步别偷懒跳过不建软链的话/usr/libexec/java_home可能识别不到这个 JDK后面的 IDE 和构建工具也会找不到它。3.3 Linuxapt、yum 与 tar.gz 任选Ubuntu/Debian 系如果系统源里有 openjdk-21可以直接sudo apt install openjdk-21-jdk但很多 LTS 系统源里并不一定带 21这时候用 Adoptium 的 APT 源更靠谱wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo tee /etc/apt/keyrings/adoptium.asc echo deb [signed-by/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F /^VERSION_CODENAME/{print $2} /etc/os-release) main | sudo tee /etc/apt/sources.list.d/adoptium.list sudo apt update sudo apt install temurin-21-jdkRHEL/CentOS/Oracle Linux 系则用 yumsudo yum install java-21-openjdk-devel注意这里必须装带-devel后缀的包只装java-21-openjdk的话很可能只有 JRE 运行时没有javac开发会直接卡住。如果想让 JDK 装在固定目录方便多版本共存用 tar.gz 解压到/optsudo mkdir -p /opt/java sudo tar -xzf jdk-21.0.6_linux-x64_bin.tar.gz -C /opt/java/ sudo ln -s /opt/java/jdk-21.0.6 /opt/java/current然后在~/.bashrc或~/.zshrc里写export JAVA_HOME/opt/java/current export PATH$JAVA_HOME/bin:$PATH3.4 JAVA_HOME 和 PATH 到底是什么我经常拿“门牌号和通讯录”来打比方。JAVA_HOME 是门牌号告诉 Maven、Gradle、IDEA 这些工具“JDK 住在哪个目录”PATH 是通讯录让终端在敲java、javac的时候不用输全路径就能直接找到可执行文件。两者配合才能同时满足“构建工具要找你”和“命令行要找到你”这两个需求。以后你看到java -version版本不对、Maven 编译用的 JDK 不对第一反应都应该是去查这两个变量而不是怀疑 JDK 没装好。八成问题都出在 JAVA_HOME 指错目录、PATH 顺序不对或者环境变量改了但终端没重启。3.5 多版本 JDK 共存怎么管理本地留多个 JDK 是常态不用卸载谁。Windows 上把每个版本放在独立目录切换版本时改 JAVA_HOME 指向即可macOS 用/usr/libexec/java_home -V查看所有版本Linux 上有update-alternatives --config java可以快速切换系统默认的 java开发环境建议再用sdkman管理它对个人开发者非常友好。但多版本共存的难点不是“能不能切”而是“项目到底用的是哪个”。所以项目级的版本控制我建议不要依赖系统默认而是在 IDE 里给项目指定 SDK或者在 Maven/Gradle 里写死 toolchain这样换设备、换人也不会跑偏。4. 安装完成后的验证与开发环境接入4.1 命令行的“三板斧”验证装完之后别急着写代码先执行下面几条java -version javac -version which java # Windows 用 where java echo $JAVA_HOME # Windows CMD 用 echo %JAVA_HOME%java -version会显示类似openjdk version 21.0.6 2025-01-21的输出javac -version能正常出现 21 字样说明开发工具链完整。which java的作用是确认当前 PATH 里真正生效的 java 路径很多人配置完 JAVA_HOME 后却发现java -version还是老版本问题往往就出在 PATH 里旧 Java 的路径排在前面。4.2 IntelliJ IDEA 接入 JDK 21IDEA 里接入很简单File→Project Structure→SDKs点加号选择 JDK把刚才安装的目录选进去。然后Project的 SDK 和 Language level 都选 21New Projects Settings里的默认 SDK 也一并改成 21。这样以后新建项目才不会又默认落到老版本。这里提醒一句IDEA 本身运行时用的是自带 JBR不是系统 JDK你在“Help → About”里看到的版本不影响项目编译。项目里用的那个 JDK永远以 Project Structure 里的设置为准。4.3 Maven、Gradle 的版本对齐命令行执行mvn -version开头那行Java version:就是当前 Maven 使用的 JDK 版本。如果这里显示的还是 17说明 JAVA_HOME 没生效或者指错了先回去修环境变量。项目里也要把 Java 版本写死。Maven 项目在 pom.xml 的 properties 里加properties java.version21/java.version /properties用 spring-boot-starter-parent 时这个属性会被自动映射到 maven-compiler-plugin 的 release 参数所以改这一个地方就够了。如果没用 Spring Boot 的 parent就手动设置maven.compiler.release21。Gradle 项目推荐用 toolchainjava { toolchain { languageVersion JavaLanguageVersion.of(21) } }团队里多版本共存时还可以给 Maven 配~/.m2/toolchains.xmltoolchains toolchain typejdk/type provides version21/version vendortemurin/vendor /provides configuration jdkHome/usr/lib/jvm/temurin-21-jdk-amd64/jdkHome /configuration /toolchain /toolchains这样环境再乱项目也能锁定自己需要的 JDK。5. 实战Spring Boot 3.5 启用虚拟线程5.1 虚拟线程解决了什么问题先回忆一下传统 Java Web 服务的并发模型Tomcat 默认每个请求占用一个平台线程平台线程对应操作系统线程默认线程池上限大概在 200 个左右。当请求里有大量 IO 等待查数据库、调 Redis、调外部接口、甚至就是Thread.sleep等待某个结果时线程大部分时间只是在“发呆”但照样占着资源。虚拟线程把这种“一个请求占一个线程”的模型彻底改掉了。虚拟线程是 JVM 管理的轻量线程通过 M:N 的方式映射到少量操作系统线程上。一个虚拟线程阻塞等待时JVM 会把它从载体线程上摘下来挂起让载体线程继续执行其他虚拟线程。打个比方以前是每个顾客配一个专属店员顾客发呆店员就干等现在是几个店员轮流服务一大群顾客谁在等菜就去服务下一位。这就是虚拟线程最核心的价值——高并发 IO 场景下你能用很低的资源消耗撑起非常高的并发请求。Spring Boot 对虚拟线程的支持从 3.2 就开始了到 3.5 版本这个配置已经完全稳定可以说是 Java 21 时代必须尝的一道菜。5.2 启用方法一行配置就够假设你新建一个 Spring Boot 3.5 项目pom.xml 里parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.0/version /parent properties java.version21/java.version /properties然后在application.yml里加spring: threads: virtual: enabled: true就这么一行。开启后 Spring Boot 内嵌容器处理请求的线程会切到虚拟线程自动配置的异步执行器也会跟着变。你不需要自己写ThreadPoolTaskExecutor不需要手工newFixedThreadPool也不需要调 Tomcat 的max-threads——在虚拟线程模型下那些都成了过去式。5.3 写个 Demo 并验证虚拟线程真的生效建一个简单的控制器模拟一个阻塞 1 秒的操作RestController public class BlockController { GetMapping(/block) public MapString, String block() throws Exception { Thread.sleep(1000); Thread thread Thread.currentThread(); return Map.of( thread, thread.getName(), isVirtual, String.valueOf(thread.isVirtual()) ); } }启动应用后访问http://localhost:8080/block返回内容里的关键字段是isVirtual。如果配置生效它应该是true而不是传统线程池下的false。线程名在不同的容器和版本里可能显示得不一样但isVirtual才是真正的硬指标。如果项目里加了 actuator还可以在配置里打开 threaddump 端点management: endpoints: web: exposure: include: health,info,threaddump然后执行curl http://localhost:8080/actuator/threaddump | grep -i virtual能看到大量虚拟线程条目说明请求确实跑在虚拟线程上了。压测的话可以用 wrkwrk -t4 -c200 -d30s http://localhost:8080/block-c200表示 200 个并发连接每个请求都阻塞 1 秒。传统线程池模型下这会直接逼近 Tomcat 默认线程上限虚拟线程模型下这是完全无压力的场景。这里我不列具体压测数字因为机器和场景不同差异很大你自己跑一遍对比开启前后同一压测命令下的表现感受会非常直观。对比项传统线程池虚拟线程并发上限受 max-threads 限制可支撑规模大得多阻塞请求占用1 个 OS 线程 约 1MB 栈轻量对象占用极小调优成本调整 core/max/queue一两行配置虚拟线程开启后原来那些“拼线程池参数”的调优经验基本可以放下你该关注的重点转移到 IO 链路本身和数据库连接池上。5.4 虚拟线程的三大红线虚拟线程不是银弹我实际用下来有三条红线必须记住第一条不要池化虚拟线程。虚拟线程的定位就是“用完即弃”因为它足够轻创建成本远低于平台线程。你如果再用线程池去管理虚拟线程等于把轻量级的优势废掉了。需要并发任务时直接Thread.startVirtualThread(...)或者用Executors.newVirtualThreadPerTaskExecutor()。第二条synchronized 会钉住载体线程。虚拟线程一旦进入 synchronized 同步块JVM 在实现上会暂时把它固定在载体线程上阻塞期间无法让出载体线程极端场景下并发能力会退化回平台线程的水平。如果临界区很短、锁竞争很低影响不大但在高性能路径上优先考虑ReentrantLock这类java.util.concurrent的锁。第三条ThreadLocal 要克制。虚拟线程数量一旦上去每个虚拟线程都持有一份 ThreadLocal 副本内存膨胀速度非常快。能用局部变量传递参数就别用 ThreadLocal真需要线程局部上下文时关注 Java 21 里 Scoped Values 这个预览特性它是针对这个场景设计的替代方案。另外还要留意如果项目里引用的第三方框架是老版本内部对“线程”有很多隐式假设开启虚拟线程前最好先看依赖版本是否兼容。实践里最常见的坑就是框架内部自己又建了平台线程池那你开启虚拟线程后会发现只有入口的请求线程变了真正干活的还是老线程池。6. 高频问题与排查记录6.1 错误现象速查表现象可能原因解决办法java -version显示旧版本PATH 里旧 JDK 排在前where java查实际路径调整 PATHjavac不是内部或外部命令装了 JRE 没装 JDK或 bin 没进 PATH换装完整 JDK确认%JAVA_HOME%\bin已配置编译和运行时报UnsupportedClassVersionError编译用的新 JDK、运行用的旧 JDK统一 JAVA_HOME重启终端CMD 里改了环境变量不生效旧窗口加载的是旧环境新开终端窗口再试mvn -version显示 Java 17JAVA_HOME 指向不对echo $JAVA_HOME检查切到 21IDEA 能跑但命令行 Maven 编译失败终端环境变量未配置按 3.3/3.4 完成环境变量设置Spring Boot 项目编译报级别错误java.version 没改或 IDEA Language level 仍为低版本pom 里写死 21IDEA 里 SDK 与 Language level 同步改虚拟线程配置了但isVirtual为 false配置拼写错误或自定义了线程池覆盖默认行为检查配置移除自定义 Executor/Tomcat 线程池参数下载速度慢或连接不上网络原因用国内镜像站华为云、阿里云、清华 TUNA 等下载Windows 的 setx 之后 PATH 丢失setx 截断长环境变量用 GUI 环境变量编辑器手动改别依赖 setxGradle 提示找不到 JDKtoolchain 未配置按 4.3 配置 toolchain 或org.gradle.java.home这里面很多问题看起来五花八门根子其实都一样环境变量和项目版本没有对齐。所以排查顺序永远是先看java -version、which java、echo $JAVA_HOME三件套再回头看 pom、gradle、IDEA 设置。6.2 一次典型的“版本错乱”排查有一次同事找我说Spring Boot 项目启动一直报错报错里出现了UnsupportedClassVersionError看起来很吓人。我先在终端里执行了mvn -version第一行就显示Java version: 17.0.12但项目 pom 里明明写着java.version21。再查echo %JAVA_HOME%指向的竟然是C:\Program Files\Java\jdk-17。问题就很清楚了他在 IDEA 里给项目选了 JDK 21但 IDEA 的终端模块默认沿用的是系统环境变量里的 JAVA_HOME所以他在 IDE 里点mvn spring-boot:run时Maven 实际跑在 JDK 17 上。依赖里某个库已经是用 Java 21 编译的17 的 JVM 加载不了就直接抛了版本不兼容错误。解决办法也不复杂把 JAVA_HOME 改到 JDK 21 目录重开终端再跑mvn -version确认版本已经变成 21然后重新启动项目。整个过程五分钟不到。这件事给我最大的启发是报错不是关键关键是建立一个“先查运行时版本”的条件反射。6.3 几条避坑心得第一下载 JDK 尽量只从正规站点或可信镜像站拿别在聊天群里收别人转发的安装包工具链被篡改的代价远比你想象的严重。第二团队统一版本号要精确到补丁级比如21.0.6而不是笼统的“Java 21”否则测试环境和生产环境补丁差异会带来诡异问题。第三个人开发机上强烈推荐用sdkman管理 JDK 版本切版本一行命令的事比手改环境变量干净太多。最后说句掏心窝的话。Java 21 的坑我这两年基本都踩过一遍从下载选版到环境变量再到虚拟线程带来的各种隐性问题。你把它当“又一个 JDK 版本”来升它就是一次普通升级你把它当“新的编程模型”来用Java 才真正从“线程池时代”迈进了“轻量并发时代”。希望这篇能把你的第一条 Java 21 跑通少走我当初走的那些弯路。
返回列表