并发模型的彻底转向

Java 21 引入虚拟线程后,Java 的并发编程范式发生了根本变化:一个请求一个线程的写法重新变得可行,而且不再需要为了吞吐去写反人类的异步回调链。

Java 25 在这一方向上继续收敛:虚拟线程的性能与可观测性更好,配套的作用域值(Scoped Values)与结构化并发(Structured Concurrency)进入更成熟的阶段,同时语言层面的模式匹配也更加完整。

虚拟线程:便宜到可以随便创建

import java.util.concurrent.Executors;
import java.time.Duration;

public class VirtualThreadsDemo {
    public static void main(String[] args) throws Exception {
        // 每个任务一个虚拟线程,无需池化
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                int id = i;
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(100)); // 阻塞时自动卸载,不占 OS 线程
                    return fetchUser(id);
                });
            }
        } // 自动等待全部完成
    }

    static String fetchUser(int id) {
        return "user-" + id;
    }
}

关键认知:

  1. 不要池化虚拟线程——它们不是稀缺资源;
  2. synchronized 的 pinning 问题已在 JDK 24 修复(JEP 491),不再会拖住载体线程;
  3. CPU 密集任务仍用平台线程池,虚拟线程只对阻塞型 IO 有效。

Scoped Values:替代 ThreadLocal 的正确方式

import java.lang.ScopedValue;

public class RequestContext {
    static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
    static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

    public Response handle(Request req) {
        // 绑定后,当前线程及所有子任务的生命周期内可见
        return ScopedValue.where(TRACE_ID, req.traceId())
                .where(CURRENT_USER, authenticate(req))
                .call(() -> process(req));
    }

    Response process(Request req) {
        // 任意深度调用栈都能直接读取,无需层层传参
        log.info("trace={}, user={}", TRACE_ID.get(), CURRENT_USER.get().name());

        try (var scope = StructuredTaskScope.open()) {
            var orders = scope.fork(() -> orderService.list(CURRENT_USER.get()));
            var coupons = scope.fork(() -> couponService.list(CURRENT_USER.get()));
            scope.join();
            return new Response(orders.get(), coupons.get());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

相比 ThreadLocal:作用域值不可变、有明确的生命周期边界、在虚拟线程大规模创建时开销远低于可变 ThreadLocal。

结构化并发:让并发任务有父子关系

import java.util.concurrent.StructuredTaskScope;

record Product(Stock stock, Price price) {}

Product loadProduct(String sku) throws Exception {
    // 任一子任务失败即整体取消,不会遗留孤儿任务
    try (var scope = StructuredTaskScope.open(
            StructuredTaskScope.Joiner.<Stock>anySuccessfulResultOrThrow())) {

        scope.fork(() -> stockClient.get(sku));
        scope.fork(() -> priceClient.get(sku));

        return scope.join();
    }
}

结构化并发解决的是“取消传播”问题:以前 ExecutorService 里的任务即使主线程超时返回,后台线程仍在跑;现在子任务的生命周期被严格限制在 try 块内。

语言层面的顺手改进

// 1. 模式匹配 + record 解构
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}

static double area(Shape s) {
    return switch (s) {
        case Circle(double r) -> Math.PI * r * r;
        case Rect(double w, double h) -> w * h;
    };
}

// 2. 灵活的构造函数体:super() 之前可以先做校验
class PositivePoint extends Point {
    PositivePoint(int x, int y) {
        if (x <= 0 || y <= 0) throw new IllegalArgumentException();
        super(x, y);
    }
}

// 3. 紧凑的源文件(隐式类 + 自动导入)
void main() {
    IO.println("Hello, Java 25");
}

升级建议

<properties>
  <maven.compiler.release>25</maven.compiler.release>
</properties>
  • 把 newFixedThreadPool 换成 newVirtualThreadPerTaskExecutor,先在数据访问层试点;
  • 用 ScopedValue 替换跨层传递的 ThreadLocal,尤其是链路追踪上下文;
  • 开启 -XX:+UseCompactObjectHeaders 与 Generational ZGC,进一步提升吞吐。

小结

Java 正在把“写高并发服务”的门槛降回到写同步代码的水平:虚拟线程负责吞吐,作用域值负责上下文,结构化并发负责生命周期。这是近十年 Java 最重要的一次体验升级,值得每个后端团队认真评估。