Java虚拟线程(Virtual Thread)在JDK 21正式发布(JEP 444),是Project Loom的核心特性。虚拟线程由JVM而非操作系统调度,创建成本接近普通对象分配,IO阻塞时自动让出载体线程(carrier thread),从根本上改变Java高并发编程模型。结构化并发(Structured Concurrency,JEP 462)在JDK 23作为预览特性引入,为虚拟线程提供错误传播和任务取消的标准化管理框架。
虚拟线程与平台线程的运行机制差异
平台线程(Platform Thread)与操作系统线程一一绑定,线程创建和上下文切换由OS内核完成,单进程线程数受限于OS线程上限(通常数千个)。虚拟线程在用户态调度,IO操作挂起时JVM自动unmount并释放载体线程,IO就绪后重新mount到可用载体线程继续执行。ForkJoinPool作为默认调度器,parallelism参数默认等于CPU核心数。
// 虚拟线程与平台线程的创建对比
import java.time.Duration;
import java.util.concurrent.*;
// 平台线程:每个线程占用约1MB栈空间
Thread platformThread = new Thread(() -> {
System.out.println("Platform thread: " + Thread.currentThread());
});
platformThread.start();
// 虚拟线程:创建成本接近零
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Virtual thread: " + Thread.currentThread());
});
// 通过Executors创建虚拟线程ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交10000个任务,虚拟线程轻松处理
int taskCount = 10_000;
var futures = new ArrayList<Future<String>>(taskCount);
for (int i = 0; i < taskCount; i++) {
final int taskId = i;
futures.add(executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // 模拟IO等待
return "Task-" + taskId + " done on " + Thread.currentThread();
}));
}
// 等待所有任务完成
for (var f : futures) {
System.out.println(f.get());
}
}
// executor自动关闭,无需手动shutdown
Executors.newVirtualThreadPerTaskExecutor()为每个任务创建一个虚拟线程,任务完成自动销毁。10000个虚拟线程的内存占用约几十MB,同等数量的平台线程需要约10GB栈空间。虚拟线程不适用于CPU密集型任务,因为计算过程不会让出载体线程,大量CPU密集型虚拟线程反而限制并行度。
结构化并发StructuredTaskScope任务管理
结构化并发通过StructuredTaskScope管理一组并发任务的生命周期,保证子任务在作用域关闭前全部完成。当某个子任务失败时,作用域可自动取消其他子任务,避免资源泄漏。ShutdownOnFailure策略在首个失败时取消所有任务,ShutdownOnSuccess策略在首个成功时取消其余任务。
// 结构化并发:并行获取用户信息并处理失败场景
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure;
record UserProfile(String name, String email, Address address) {}
record Address(String city, String street) {}
// 模拟远程API调用
UserProfile fetchUser(String userId) throws Exception {
Thread.sleep(50); // 模拟网络延迟
return new UserProfile("user-" + userId, "user@test.com",
new Address("Beijing", "Zhongguancun"));
}
Address fetchAddress(String userId) throws Exception {
Thread.sleep(80); // 模拟网络延迟
return new Address("Beijing", "Zhongguancun");
}
UserProfile getUserProfile(String userId) throws Exception {
try (var scope = new ShutdownOnFailure()) {
// 并行发起多个子任务
var userTask = scope.fork(() -> fetchUser(userId));
var addressTask = scope.fork(() -> fetchAddress(userId));
// 等待所有子任务完成(或首个失败)
scope.join();
scope.throwIfFailed(); // 有子任务失败时抛出异常
// 所有子任务成功,组装结果
var user = userTask.get();
var address = addressTask.get();
return new UserProfile(user.name(), user.email(), address);
}
// scope.close()自动取消所有未完成子任务
}
StructuredTaskScope的join()方法等待所有子任务完成,throwIfFailed()在任一子任务抛出异常时将异常传播到父作用域。这种模式确保并发任务组具有原子性语义——要么全部成功,要么整体失败并清理资源,避免部分成功的中间状态。
线程池迁移策略与性能调优要点
从传统线程池迁移到虚拟线程需要注意几个关键差异。虚拟线程不需要池化,每次创建新虚拟线程即可,Executors.newFixedThreadPool和newCachedThreadPool的模式不再需要。ThreadLocal在虚拟线程场景下可能产生内存泄漏——大量虚拟线程各自持有ThreadLocal副本,建议使用Scoped Value(JEP 446)替代。
// 现有CompletableFuture与虚拟线程配合
var executor = Executors.newVirtualThreadPerTaskExecutor();
var futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(
() -> fetchUrl(url), executor
))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).join();
// Spring Boot启用虚拟线程(application.properties)
// spring.threads.virtual.enabled=true
// Tomcat将为每个HTTP请求分配虚拟线程
// 注意:synchronized块会pin虚拟线程(JDK 21)
// JDK 24移除了synchronized pinning,但JDK 21建议改用ReentrantLock
import java.util.concurrent.locks.ReentrantLock;
private final ReentrantLock lock = new ReentrantLock();
public void safeMethod() {
lock.lock();
try {
// 替代synchronized(this),避免虚拟线程pinning
blockingOperation();
} finally {
lock.unlock();
}
}
Spring Boot 3.2+通过spring.threads.virtual.enabled=true配置项自动在Tomcat、@Async和@Scheduled中使用虚拟线程。迁移后QPS提升通常在2到5倍之间,具体取决于IO等待占比。监控层面关注载体线程的park/unpark频率和调度延迟,通过JFR(Java Flight Recorder)录制分析虚拟线程的执行轨迹。虚拟线程不改变线程安全要求,竞态条件和死锁风险依然存在,并发控制逻辑需要同步审查。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/java-xu-ni-xian-cheng-projectloom-jie-gou-hua-bing-fa-yu/