并发演进(JDK 8 → 25)#
版本信息
- 范围:跨版本专题,串起散落在 JDK 8/21/25 的并发模型变化
- 关键 JEP:444: Virtual Threads(21)|506: Scoped Values(25)|505: Structured Concurrency(25,预览)|266: CompletableFuture(8)|Flow 反应式 API(9)
并发是 Java 这十多年演进最剧烈的领域,但变化散落在多个版本,初学者容易只见树木不见森林。本专题把它们串成一条主线:线程模型、线程局部、任务编排三条线,各自怎么从 JDK 8 演进到 25。
演进主线#
graph LR
subgraph M1["线程模型"]
A8["平台线程(JDK 8)"] --> A21["虚拟线程(JDK 21)"]
end
subgraph M2["线程局部"]
B8["ThreadLocal(JDK 8)"] --> B25["ScopedValue(JDK 25)"]
end
subgraph M3["任务编排"]
C8["Future / CompletableFuture(JDK 8)"] --> C25["结构化并发(JDK 25 预览)"]
end
A21 -.->|载体线程| B25
style A21 fill:#c8e6c9
style B25 fill:#c8e6c9
style C25 fill:#fff9c4
线索一:线程模型——平台线程 → 虚拟线程#
JDK 8 的 Thread 是平台线程(1:1 映射 OS 线程,栈 ~1MB),上千个就吃光内存;于是高并发被迫上 CompletableFuture 链/响应式框架,代码可读性骤降(回调地狱)。JDK 21 的虚拟线程是用户态轻量线程,阻塞时自动卸载、让载体跑别的虚拟线程(M:N),用同步阻塞写法达到异步吞吐,单 JVM 可承载数十万级虚拟线程。详见 虚拟线程(JDK 21)。
// JDK 8:平台线程,阻塞即占资源
new Thread(() -> { blockingIo(); }).start();
// JDK 21:虚拟线程,阻塞时自动让出载体
Thread.startVirtualThread(() -> { blockingIo(); });
线索二:线程局部——ThreadLocal → ScopedValue#
「把数据绑定到当前线程」在 JDK 8 靠 ThreadLocal:可变、生命周期无界、易内存泄漏,且与海量虚拟线程不搭(可变共享破坏不可变性假设)。JDK 25 的 ScopedValue 提供不可变、词法作用域的绑定——进入作用域有效、退出自动解绑,天然适合虚拟线程与结构化并发。详见 Scoped Values(JDK 25)。
// JDK 8:ThreadLocal,可变 + 必须 remove 防泄漏
static final ThreadLocal<String> USER = new ThreadLocal<>();
USER.set("alice"); try { use(); } finally { USER.remove(); }
// JDK 25:ScopedValue,不可变 + 作用域结束自动解绑
static final ScopedValue<String> USER = ScopedValue.newInstance();
ScopedValue.where(USER, "alice").run(() -> use());
线索三:任务编排——CompletableFuture → 结构化并发#
JDK 8 的 CompletableFuture(JEP 266)把 Future 升级为可组合/回调,但仍易散落、错误处理繁琐、取消难传播。JDK 25 的结构化并发(JEP 505,预览)让并发的子任务像代码块一样有界:用 StructuredTaskScope 打开一个作用域,fork 子任务、join 等全部完成、关闭即保证没有子任务逃逸,错误与取消沿作用域传播。它与虚拟线程、ScopedValue 是一套。
// JDK 8:CompletableFuture,回调/组合,取消与错误传播弱
var a = CompletableFuture.supplyAsync(this::fetchA);
var b = CompletableFuture.supplyAsync(this::fetchB);
a.thenCombine(b, this::merge); // 散落、出错要各自 exceptionally
// JDK 25(预览):结构化并发,作用域内有界、错误/取消自动传播
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // 预览 API
var a = scope.fork(this::fetchA); // 子任务(默认虚拟线程)
var b = scope.fork(this::fetchB);
scope.join(); // 等全部完成
return new Result(a.get(), b.get());
} // 离开作用域:无子任务逃逸,资源/取消自动清理
底层原理:为什么这三条线要一起演进#
虚拟线程解决了「线程太贵」,但旧并发工具是按「少量平台线程」设计的:ThreadLocal 的可变共享与海量虚拟线程冲突 → 需要 ScopedValue 的不可变;CompletableFuture/ExecutorService 的散落提交与虚拟线程的「用完即弃」不搭 → 需要结构化并发的「有界作用域」。三者构成 JDK 25 的并发新范式:同步阻塞写法 + 海量虚拟线程 + 不可变作用域值 + 结构化编排,让「高吞吐」与「可读、可维护」兼得。
常见坑 / 最佳实践#
- 虚拟线程别池化:用
Executors.newVirtualThreadPerTaskExecutor(),一任务一线程、用完即弃;别套synchronized(JDK 21 钉住载体),IO 密集用ReentrantLock。 - 新代码优先
ScopedValue:尤其配合虚拟线程;遗留ThreadLocal仍可用,但别在新设计扩散。 - 结构化并发仍是预览(JDK 25):需
--enable-preview,API 仍在调整(如StructuredTaskScope改用静态工厂开);生产前盯紧后续版本转正。 - 虚拟线程 ≠ 更快:它提升并发吞吐(更多等待型任务并行),不提升CPU 计算吞吐(CPU 密集任务不受益)。
小结#
并发的三步演进——线程(平台→虚拟)、局部(ThreadLocal→ScopedValue)、编排(散落→结构化)——让 Java 在 JDK 25 拥有了「同步写法 + 高吞吐 + 可维护」的现代并发范式。本专题把它们串起来,单点的 虚拟线程、Scoped Values 页有各自的可运行示例。