JEP
JDK 25 于 2025 年 9 月 16 日 发布,这是一个非常重要的版本,里程碑式。
JDK 25 是 LTS(长期支持版),至此为止,有JDK8、JDK11、JDK17、JDK21和 JDK 25 这五个长期支持版了。
JDK 21 共有 18 个新特性,这篇文章会挑选其中较为重要的一些新特性进行详细介绍:
- JEP 506: Scoped Values (作用域值)
- JEP 512: Compact Source Files and Instance Main Methods (紧凑源文件与实例主方法)
- JEP 519: Compact Object Headers (紧凑对象头)
- JEP 521: Generational Shenandoah (分代 Shenandoah GC)
- JEP 507: Primitive Types in Patterns, instanceof, and switch (模式匹配支持基本类型, 第三次预览)
- JEP 505: Structured Concurrency (结构化并发, 第五次预览)
- JEP 511: Module Import Declarations (模块导入声明)
- JEP 513: Flexible Constructor Bodies (灵活的构造函数体)
- JEP 508: Vector API (向量API, 第十次孵化)
下图是从 JDK 8 到 JDK 24 每个版本的更新带来的新特性数量和更新时间:
JEP 506: 作用域值
作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量 ThreadLocal
,尤其是在使用大量虚拟线程时。
final static ScopedValue<...> V = new ScopedValue<>();
// In some method
ScopedValue.where(V, <value>)
.run(() -> { ... V.get() ... call methods ... });
// In a method called directly or indirectly from the lambda expression
... V.get() ...
作用域值通过其“写入时复制”(copy-on-write)的特性,保证了数据在线程间的隔离与安全,同时性能极高,占用内存也极低。这个特性将成为未来 Java 并发编程的标准实践。
JEP 512: 紧凑源文件与实例主方法
该特性第一次预览是由 JEP 445 (JDK 21 )提出,随后经过了 JDK 22 、JDK 23 和 JDK 24 的改进和完善,最终在 JDK 25 顺利转正。
这个改进极大地简化了编写简单Java程序的步骤,允许将类和主方法写在同一个没有顶级 public class
的文件中,并允许 main
方法成为一个非静态的实例方法。
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
进一步简化:
void main() {
System.out.println("Hello, World!");
}
这是为了降低Java的学习门槛和提升编写小型程序、脚本的效率而迈出的一大步。初学者不再需要理解 public static void main(String[] args)
这一长串复杂的声明。对于快速原型验证和脚本编写,这也使得 Java 成为一个更有吸引力的选择。
JEP 519: 紧凑对象头
该特性第一次预览是由 JEP 450 (JDK 24 )提出,JDK 25 就顺利转正了。
通过优化对象头的内部结构,在 64 位架构的 HotSpot 虚拟机中,将对象头大小从原本的 96-128 位(12-16 字节)缩减至 64 位(8 字节),最终实现减少堆内存占用、提升部署密度、增强数据局部性的效果。
紧凑对象头并没有成为JVM 默认的对象头布局方式,需通过显式配置启用:
- JDK 24 需通过命令行参数组合启用:
$ java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders ...
; - JDK 25 之后仅需
-XX:+UseCompactObjectHeaders
即可启用。
JEP 521: 分代 Shenandoah GC
Shenandoah GC 在 JDK12 中成为正式可生产使用的 GC,默认关闭,通过 -XX:+UseShenandoahGC
启用。
Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等
传统的Shenandoah对整个堆进行并发标记和整理,虽然暂停时间极短,但在处理年轻代对象时效率不如分代GC。引入分代后,Shenandoah可以更频繁、更高效地回收年轻代中的大量“朝生夕死”的对象,使其在保持极低暂停时间的同时,拥有了更高的吞吐量和更低的CPU开销。
Shenandoah GC 需要通过命令启用:
- JDK 24 需通过命令行参数组合启用:
-XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions -XX:ShenandoahGCMode=generational
- JDK 25 之后仅需
-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
即可启用。
JEP 507: 模式匹配支持基本类型 (第三次预览)
该特性第一次预览是由 JEP 455 (JDK 23 )提出。
模式匹配可以在 switch
和 instanceof
语句中处理所有的基本数据类型(int
, double
, boolean
等)
static void test(Object obj) {
if (obj instanceof int i) {
System.out.println("这是一个int类型: " + i);
}
}
这样就可以像处理对象类型一样,对基本类型进行更安全、更简洁的类型匹配和转换,进一步消除了 Java 中的模板代码。
JEP 505: 结构化并发(第五次预览)
JDK 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代java.util.concurrent
,目前处于孵化器阶段。
结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。
结构化并发的基本 API 是StructuredTaskScope
,它支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。
StructuredTaskScope
的基本用法如下:
try (var scope = new StructuredTaskScope<Object>()) {
// 使用fork方法派生线程来执行子任务
Future<Integer> future1 = scope.fork(task1);
Future<String> future2 = scope.fork(task2);
// 等待线程完成
scope.join();
// 结果的处理可能包括处理或重新抛出异常
... process results/exceptions ...
} // close
结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
JEP 511: 模块导入声明
该特性第一次预览是由 JEP 476 (JDK 23 )提出,随后在 JEP 494 (JDK 24)中进行了完善,JDK 25 顺利转正。
模块导入声明允许在 Java 代码中简洁地导入整个模块的所有导出包,而无需逐个声明包的导入。这一特性简化了模块化库的重用,特别是在使用多个模块时,避免了大量的包导入声明,使得开发者可以更方便地访问第三方库和 Java 基本类。
此特性对初学者和原型开发尤为有用,因为它无需开发者将自己的代码模块化,同时保留了对传统导入方式的兼容性,提升了开发效率和代码可读性。
// 导入整个 java.base 模块,开发者可以直接访问 List、Map、Stream 等类,而无需每次手动导入相关包
import module java.base;
public class Example {
public static void main(String[] args) {
String[] fruits = { "apple", "berry", "citrus" };
Map<String, String> fruitMap = Stream.of(fruits)
.collect(Collectors.toMap(
s -> s.toUpperCase().substring(0, 1),
Function.identity()));
System.out.println(fruitMap);
}
}
JEP 513: 灵活的构造函数体
该特性第一次预览是由 JEP 447 (JDK 22)提出,随后在 JEP 482 (JDK 23)和 JEP 492 (JDK 24)经历了预览,JDK 25 顺利转正。
Java 要求在构造函数中,super(...)
或 this(...)
调用必须作为第一条语句出现。这意味着我们无法在调用父类构造函数之前在子类构造函数中直接初始化字段。
灵活的构造函数体解决了这一问题,它允许在构造函数体内,在调用 super(..)
或 this(..)
之前编写语句,这些语句可以初始化字段,但不能引用正在构造的实例。这样可以防止在父类构造函数中调用子类方法时,子类的字段未被正确初始化,增强了类构造的可靠性。
这一特性解决了之前 Java 语法限制了构造函数代码组织的问题,让开发者能够更自由、更自然地表达构造函数的行为,例如在构造函数中直接进行参数验证、准备和共享,而无需依赖辅助方法或构造函数,提高了代码的可读性和可维护性。
class Person {
private final String name;
private int age;
public Person(String name, int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative.");
}
this.name = name; // 在调用父类构造函数之前初始化字段
this.age = age;
// ... 其他初始化代码
}
}
class Employee extends Person {
private final int employeeId;
public Employee(String name, int age, int employeeId) {
this.employeeId = employeeId; // 在调用父类构造函数之前初始化字段
super(name, age); // 调用父类构造函数
// ... 其他初始化代码
}
}
JEP 508: 向量API(第十次孵化)
向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。
向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。
这是对数组元素的简单标量计算:
void scalarComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
这是使用 Vector API 进行的等效向量计算:
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
void vectorComputation(float[] a, float[] b, float[] c) {
int i = 0;
int upperBound = SPECIES.loopBound(a.length);
for (; i < upperBound; i += SPECIES.length()) {
// FloatVector va, vb, vc;
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.intoArray(c, i);
}
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
尽管仍在孵化中,但其第十次迭代足以证明其重要性。它使得Java在科学计算、机器学习、大数据处理等性能敏感领域,能够编写出接近甚至媲美C++等本地语言性能的代码。这是Java在高性能计算领域保持竞争力的关键。
舍弃ThreadLocal
ScopedValues 让线程安全的上下文管理更简单、更高效
在Java开发中,我们经常需要在多个方法之间传递上下文信息,比如用户ID、请求ID、事务信息等。传统做法是使用ThreadLocal
,但它存在诸多问题。
ThreadLocal的痛点
1. 内存泄漏风险
// 传统ThreadLocal使用
ThreadLocal<String> userContext = new ThreadLocal<>();
void main() {
for (int i = 1; i <= 10000; i++) {
userContext.set("user123");
// 如果忘记调用remove(),可能导致内存泄漏
// userContext.remove();
}
}
2. 上下文继承复杂
- • 子线程无法自动继承父线程的上下文
- • 需要额外的InheritableThreadLocal处理
- • 异步任务中上下文传递困难,需要手动传参
- • 线程池复用时可能残留旧的上下文数据
3. 虚拟线程性能问题
- • ThreadLocal在虚拟线程中表现糟糕
- • 占用大量内存资源
- • 影响虚拟线程的轻量级特性
ScopedValues:完美替代方案
核心特性
- • 🚀 性能更优:专为现代并发设计
- • 🔒 内存安全:自动管理生命周期
- • 🌟 虚拟线程友好:完美支持Project Loom
- • 📦 不可变设计:一旦绑定,无法修改
💻 实战对比:从ThreadLocal到ScopedValues
ThreadLocal实现方式
public class ThreadLocalExample {
private static final ThreadLocal<String> USER_CONTEXT =
new ThreadLocal<>();
public void processRequest(String userId) {
USER_CONTEXT.set(userId);
try {
businessLogic();
} finally {
// 必须手动清理,否则内存泄漏
USER_CONTEXT.remove();
}
}
public void businessLogic() {
String userId = USER_CONTEXT.get();
System.out.println("Processing for user: " + userId);
}
}
ScopedValues实现方式
private static final ScopedValue<String> USER_CONTEXT =
ScopedValue.newInstance();
public void businessLogic() {
String userId = USER_CONTEXT.get();
System.out.println("Processing for user: " + userId);
}
void main() {
// 自动管理生命周期,无需手动清理
ScopedValue.where(USER_CONTEXT, "User123")
.run(this::businessLogic);
}
高级应用场景
1. Web请求上下文管理
private static final ScopedValue<String> REQUEST_ID =
ScopedValue.newInstance();
private static final ScopedValue<String> USER_ID =
ScopedValue.newInstance();
private void processBusinessLogic() {
// 可以在任何嵌套方法中访问上下文
System.out.println("Processing request: " + REQUEST_ID.get() +
" for user: " + USER_ID.get());
}
void main() {
ScopedValue.where(REQUEST_ID, "requestId")
.where(USER_ID, "userId")
.run(this::processBusinessLogic);
}
2. 异步任务上下文传播
private static final ScopedValue<String> TRACE_ID =
ScopedValue.newInstance();
// 创建异步任务
public CompletableFuture<String> asyncProcess(String traceId) {
return CompletableFuture.supplyAsync(() -> {
// 在异步线程中重新绑定ScopedValue
return ScopedValue.where(TRACE_ID, traceId)
.call(() -> {
// 现在可以安全地使用ScopedValue
var result = "Result for trace: " + TRACE_ID.get();
System.out.println(result);
return result;
});
});
}
void main() throws Exception {
// 主线程调用传递给子线程
asyncProcess("TRACE_ID_1").get();
}
📊 性能对比数据
特性 | ThreadLocal | ScopedValues |
---|---|---|
内存开销 | 高 | 低 |
虚拟线程支持 | 差 | 优秀 |
安全性 | 需手动管理 | 自动管理 |
性能 | 一般 | 更快 |
🛠️ 最佳实践建议
1. 静态声明ScopedValue
// 正确做法
private static final ScopedValue<String> CONTEXT =
ScopedValue.newInstance();
// 避免这样做
private ScopedValue<String> instanceContext =
ScopedValue.newInstance();
2. 合理的作用域范围
public void correctScope() {
ScopedValue.where(CONTEXT, "value")
.run(() -> {
// 在这个作用域内,CONTEXT有效
doSomething();
// 作用域结束后,自动清理
});
// 这里CONTEXT已经不可访问
}
3. 异常处理
public void exceptionHandling() {
try {
ScopedValue.where(CONTEXT, "value")
.run(() -> {
riskyOperation();
});
} catch (Exception e) {
// 即使出现异常,ScopedValue也会自动清理
handleException(e);
}
}
虚拟线程
Java 25虚拟线程解析:@Async注解的正确打开方式
随着Java 25的发布,虚拟线程迎来了重大升级!新版本不仅解决了早期版本的性能瓶颈,还引入了多项革命性优化,让虚拟线程在Spring应用中的表现更加出色。
现代Spring应用程序都面临着同样的难题:如何在不消耗过多CPU资源或阻塞线程的情况下,快速运行大量任务?
提到并发处理,我们经常听到三个概念:虚拟线程、线程池和 @Async注解。很多开发者容易将它们混淆,认为它们是同一件事。
其实不然!
- • 虚拟线程(Virtual Threads):JVM的一种执行模型
- • 线程池(Thread Pools):资源管理器
- • @Async注解:Spring的任务路由工具,将工作分发给你选择的执行器
理解它们的区别对于应用的延迟、云成本和可靠性至关重要。选择错误可能导致P95延迟飙升、队列堆积和超时问题。
什么是虚拟线程?
虚拟线程它们在阻塞I/O操作时能够快速地地暂停和恢复。
核心优势
- • 高并发:可以创建数百万个虚拟线程
- • 低成本:内存占用极小(约8KB vs 传统线程的2MB)
- • 高效I/O:在I/O阻塞时自动让出CPU资源
适用场景
// 适合:大量I/O密集型任务
@Async
public CompletableFuture<String> fetchDataFromAPI(String url) {
// 网络请求、数据库查询等
return restTemplate.getForObject(url, String.class);
}
🏊♂️ 什么是线程池?
线程池是一种资源管理策略,通过预先创建固定数量的线程来处理任务,避免频繁创建和销毁线程的开销。
配置示例
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-task-");
executor.initialize();
return executor;
}
}
🔄 @Async注解的作用
@Async是Spring提供的任务路由器,它将方法调用转发给指定的执行器处理。
基本用法
@Service
public class UserService {
@Async("taskExecutor")
public CompletableFuture<User> processUser(Long userId) {
// 异步处理逻辑
User user = userRepository.findById(userId);
// 耗时操作...
return CompletableFuture.completedFuture(user);
}
}
配置虚拟线程执行器
Spring Boot 3.5 配置
要使用虚拟线程功能,需要满足以下版本要求:
- • Java 21+:虚拟线程正式版本
- • Spring Boot 3.2+:完整支持虚拟线程配置
- • Spring Framework 6.1+:底层框架支持
@Configuration
@EnableAsync
public class VirtualThreadConfig {
@Bean(name = "virtualThreadExecutor")
public TaskExecutor virtualThreadExecutor() {
return new TaskExecutorAdapter(
Executors.newVirtualThreadPerTaskExecutor()
);
}
@Bean(name = "fixedThreadPool")
public TaskExecutor fixedThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(40);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("fixed-pool-");
executor.initialize();
return executor;
}
}
在Service中使用
@Service
public class DataProcessingService {
// 使用虚拟线程处理I/O密集型任务
@Async("virtualThreadExecutor")
public CompletableFuture<String> fetchExternalData(String url) {
// 网络请求逻辑
return CompletableFuture.completedFuture(
restTemplate.getForObject(url, String.class)
);
}
// 使用传统线程池处理CPU密集型任务
@Async("fixedThreadPool")
public CompletableFuture<Integer> calculateHash(String data) {
// CPU密集型计算
return CompletableFuture.completedFuture(
data.hashCode() * complexCalculation(data)
);
}
}
注意事项
虚拟线程使用陷阱
-
-
避免在虚拟线程中使用synchronized
- • 可能导致平台线程被固定(pinning)
- • 推荐使用ReentrantLock
-
-
-
谨慎使用ThreadLocal
- • 虚拟线程数量巨大,可能导致内存泄漏
-
性能监控
@Component
public class ThreadMonitor {
@EventListener
public void handleAsyncTaskExecution(AsyncTaskExecutionEvent event) {
log.info("Task executed on: {} thread",
Thread.currentThread().isVirtual() ? "virtual" : "platform");
}
}
总结
在Spring应用中选择合适的并发模型需要考虑以下几点:
虚拟线程 vs 线程池:如何选择?
对比维度 | 虚拟线程 | 传统线程池 |
---|---|---|
适用任务类型 | I/O密集型任务 | CPU密集型任务 |
并发能力 | 支持数百万并发 | 受限于线程池大小 |
内存占用 | 极低(约8KB/线程) | 较高(约2MB/线程) |
创建成本 | 几乎为零 | 有一定开销 |
阻塞处理 | 自动让出CPU | 阻塞整个线程 |
资源控制 | 难以精确控制 | 可精确控制并发数 |
适用场景 | • 微服务API调用 • 数据库查询 • 文件读写 • 网络通信 • 高并发请求处理 | • 复杂计算 • 图像处理 • 数据分析 • 需要限制并发数 • 对资源有严格控制要求 |
虚拟线程并不是银弹,传统线程池也有其存在价值。关键是要根据具体场景选择合适的工具,并通过@Async注解灵活地进行任务路由。
虚拟线程使用
JDK 25(从 JDK 19 开始预览,JDK 21 正式 GA)里引入了 虚拟线程(Virtual Threads),这是 Project Loom 的核心成果之一,算是 Java 平台几十年来对并发编程最大的改造之一
1️⃣ 背景
传统 Java 线程是操作系统线程的直接映射(OS thread),特点是:
- 创建开销大(要向内核申请资源,栈空间固定分配 1M 左右)
- 切换成本高(线程上下文切换依赖操作系统调度)
- 数量有限(几千到几万就容易打满内核调度能力)
👉 在高并发 I/O 场景(比如百万级 WebSocket、RPC 调用)下,传统线程模型成本太高。
虚拟线程的目标:
- 让开发者还能用熟悉的 线程 API(
Thread
、ExecutorService
等),但背后是轻量级、用户态的调度。 - 能在同一 JVM 内创建上百万个并发任务,而不会像原来那样被操作系统卡住。
2️⃣ 原理
虚拟线程 ≠ 操作系统线程,而是运行在 JVM 调度器管理的用户态线程。
🔹 关键点:
- 用户态调度:虚拟线程由 JVM 自己的调度器管理,而不是操作系统
- M:N 映射:很多虚拟线程 (M) 映射到少量平台线程 (N) 上执行
- 可挂起、可恢复:当虚拟线程遇到阻塞操作(I/O、
sleep
),JVM 会把它挂起,释放底层的 OS 线程,等 I/O 完成后再恢复执行 - 栈分片(Stack Chunking):不像传统线程预分配 1M 栈,虚拟线程的栈是分块、可扩展的,更省内存
📌 类似 Go 的 Goroutine、Kotlin 的协程(Coroutines),但 API 保持了 Java 一致性。
3️⃣ 使用方式
JDK 21 起,虚拟线程正式可用。几种典型写法:
(1)直接创建
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Hello from virtual thread: " + Thread.currentThread());
});
vt.join();
(2)虚拟线程池(每个任务一个线程)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
int id = i;
executor.submit(() -> {
Thread.sleep(1000);
System.out.println("Task " + id + " done by " + Thread.currentThread());
return id;
});
}
}
这里可以提交上百万个任务,几乎不会 OOM。
(3)结构化并发 API
JDK 21 新引入 StructuredTaskScope
,让虚拟线程的管理更优雅。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> findUser());
Future<Integer> order = scope.fork(() -> fetchOrders());
scope.join(); // 等待所有任务完成
scope.throwIfFailed(); // 如果有任务失败,抛出异常
System.out.println("User: " + user.resultNow());
System.out.println("Orders: " + order.resultNow());
}
4️⃣ 注意事项
- 适合 I/O 密集型任务,不适合 CPU 密集型
- 虚拟线程切换时遇到 阻塞 I/O 才会释放 OS 线程
- 如果是纯 CPU 计算,还是受限于 CPU 核数
- 阻塞 API 自动挂起
- 像
Socket.read()
、Thread.sleep()
等传统阻塞 API,虚拟线程中会被安全地“挂起” - 但一些 本地调用(JNI)、synchronized 锁 可能会阻塞 OS 线程,降低效果
- 像
- 监控与调试
- 一个进程可能有数百万个虚拟线程,传统的监控工具(jstack、JConsole)可能会卡爆
- JDK 提供了专门的工具改进(如
jcmd Thread.print -v
)
5️⃣ 适用场景
- ✅ 高并发 I/O 服务(RPC、Web 服务、消息队列客户端)
- ✅ 数据库访问(JDBC 阻塞 API)
- ✅ 批量请求聚合(调用外部 API)
- ❌ 不适合纯计算密集型任务(比如大数据批处理、机器学习)
6️⃣ 总结一句话
虚拟线程让“每个请求一个线程”的模型在 Java 里变得可行。
以前必须用异步回调(Netty、CompletableFuture)的场景,现在直接用同步阻塞写法就能支持百万并发。