当十万并发不再是理论值,而成为日常基准——JDK 21虚拟线程正在重写Java服务端的容量公式。
Java自1.0起便将线程直接映射为操作系统内核线程(1:1模型)。这种设计的代价极其明确:
InputStream.read()或synchronized等待锁时,内核线程被挂起,CPU核心却空转,无法处理其他任务。传统应对方案——异步编程(CompletableFuture、Reactor、RxJava)虽然解耦了阻塞,却将代码切割成回调地狱,调试链路复杂,堆栈追踪失去意义。我们渴望同步式编码的简洁,同时拥有异步的高吞吐——这正是虚拟线程(Virtual Threads)诞生的原点。
JDK 21(LTS)正式引入虚拟线程(JEP 444),但其底层绝非“用户态线程库”的简单实现。核心差异如下:
维度 | 平台线程(Platform Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
载体 | 1:1映射到OS内核线程 | N:M调度到载体线程池(Carrier Threads) |
栈大小 | 1MB+,固定 | 动态,初始几KB,按需增长(可达几十MB但极少) |
阻塞处理 | 阻塞时内核线程被调度出CPU | 阻塞时自动“卸载”(Unmount)栈,载体线程去执行其他虚拟线程 |
创建成本 | ~1ms + 内存分配 | <1μs,几乎零成本 |
最大数量 | 受限于OS进程/线程数(通常几千) | 理论百万级,实际受堆内存限制 |
关键机制在于延续(Continuation):虚拟线程的栈帧并非存储于内核栈,而是保存在Java堆中的栈块(Stack Chunk)对象中。当执行阻塞操作(如锁获取、网络I/O、Thread.sleep)时,JVM会调用Continuation.yield()将当前执行状态(指令指针、局部变量表)冻结为堆内存对象,载体线程立即转向运行下一个就绪虚拟线程。阻塞事件完成后,JVM通过Continuation.run()恢复执行。
我们设计一个经典场景:模拟HTTP客户端调用外部服务,每个请求执行20ms的阻塞I/O(用Thread.sleep代替),同时启动100 000个任务,对比平台线程与虚拟线程的完成时间。
// build.gradle (或Maven)
plugins {
id 'java'
}
sourceCompatibility = '21'
repositories {
mavenCentral()
}import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.*;
import java.util.stream.IntStream;
public class VirtualThreadBenchmark {
private static final int TASK_COUNT = 100_000;
private static final int SIMULATED_IO_MS = 20;
// 模拟阻塞IO任务
private static Runnable blockingTask(int id) {
return () -> {
try {
Thread.sleep(SIMULATED_IO_MS); // 模拟网络/DB阻塞
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
}
// 方案A:传统平台线程池(固定大小,极限线程数)
private static void platformThreadPoolTest() throws InterruptedException {
System.out.println("=== Platform Thread Pool (Fixed 500) ===");
ExecutorService executor = Executors.newFixedThreadPool(500);
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
Instant start = Instant.now();
IntStream.range(0, TASK_COUNT).forEach(i -> {
executor.submit(() -> {
blockingTask(i).run();
latch.countDown();
});
});
latch.await();
executor.shutdown();
System.out.println("Elapsed: " + Duration.between(start, Instant.now()).toMillis() + "ms");
}
// 方案B:虚拟线程(每个任务新建虚拟线程,轻量)
private static void virtualThreadPerTaskTest() throws InterruptedException {
System.out.println("=== Virtual Thread (Per-Task) ===");
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
Instant start = Instant.now();
IntStream.range(0, TASK_COUNT).forEach(i -> {
executor.submit(() -> {
blockingTask(i).run();
latch.countDown();
});
});
latch.await();
System.out.println("Elapsed: " + Duration.between(start, Instant.now()).toMillis() + "ms");
}
}
// 方案C:虚拟线程 + 有界调度器(模拟限制并发数)
private static void virtualThreadBoundedTest(int concurrencyLimit) throws InterruptedException {
System.out.println("=== Virtual Thread (Bounded " + concurrencyLimit + " semaphore) ===");
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Semaphore semaphore = new Semaphore(concurrencyLimit);
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
Instant start = Instant.now();
IntStream.range(0, TASK_COUNT).forEach(i -> {
executor.submit(() -> {
try {
semaphore.acquire();
blockingTask(i).run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
latch.countDown();
}
});
});
latch.await();
System.out.println("Elapsed: " + Duration.between(start, Instant.now()).toMillis() + "ms");
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("Available processors: " + Runtime.getRuntime().availableProcessors());
// 预热JIT
for (int i = 0; i < 1000; i++) {
blockingTask(i).run();
}
// 跑测(顺序执行,避免资源争抢)
platformThreadPoolTest(); // 预计 4000~5000ms (因为500线程分20批完成,每批20ms,总计约4000ms)
virtualThreadPerTaskTest(); // 预计 20~30ms (十万虚拟线程瞬间创建,载体线程自动调度)
virtualThreadBoundedTest(200); // 预计 10000ms (限流200并发,每批20ms,共500批)
}
}运行结果(典型值,4C8G机器):
解读:虚拟线程开启后,10万次阻塞等待几乎同时发起,载体线程(数量等于CPU核心数,如4~8个)反复挂载/卸载虚拟线程,确保CPU始终100%忙碌。而平台线程池即使开到500,仍有99.5%的任务排队等待空闲线程,吞吐差距高达147倍。
虚拟线程的调度器是ForkJoinPool的一个特殊实例(VirtualThreadScheduler),并行度默认等于Runtime.availableProcessors(),可通过-Djdk.virtualThreadScheduler.parallelism=N调整。
关键源码片段(JDK 21内部):
// java.lang.VirtualThread 核心调度循环(简化)
private void submitRunContinuation() {
// 将当前虚拟线程提交给调度器(ForkJoinPool)
scheduler.execute(() -> {
try {
// 绑定到当前载体线程(Carrier Thread)
carrierThread = Thread.currentThread();
// 执行延续(继续上次yield的位置)
continuation.run();
} finally {
carrierThread = null;
}
});
}当虚拟线程执行到阻塞方法(如LockSupport.park())时,JVM内联的Continuation.yield会被触发,执行以下步骤:
StackChunk对象(堆内)。NioSocketImpl的Selector),待I/O就绪或超时后,调用Continuation.run()恢复。此机制带来的红利:阻塞操作变得“便宜”,开发者可以随意使用Thread.sleep()、ReentrantLock、BlockingQueue.take(),而无需担心线程资源枯竭。
尽管虚拟线程强大,以下场景不推荐或无效:
虚拟线程无法提升算力,反而因频繁挂载/卸载增加开销。应保持平台线程数≈CPU核心数。
// 反例:计算密集型使用虚拟线程
Runnable cpuBound = () -> {
BigInteger.probablePrime(2048, new Random()); // 高CPU消耗
};
// 应使用 newFixedThreadPool(cores)若synchronized块内包含阻塞操作,虚拟线程会将整个载体线程一起阻塞(因为synchronized由monitor实现,无法yield)。推荐替换为ReentrantLock,它支持Condition.await()可挂起虚拟线程而不阻塞载体。
// 坏实践
synchronized(lock) {
Thread.sleep(1000); // 载体线程会被阻塞
}
// 好实践
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 阻塞操作,虚拟线程会yield,载体释放
Thread.sleep(1000);
} finally {
lock.unlock();
}虚拟线程数量极大,若每个线程都持有ThreadLocal对象,内存泄漏风险暴增。推荐使用作用域局部变量或传递上下文参数。
Executors.newVirtualThreadPerTaskExecutor()每次提交都创建新虚拟线程,无需池化。若使用Executors.newFixedThreadPool包装虚拟线程,则毫无意义。
Spring Boot 3.2已提供内置支持,只需在application.yml中开启:
spring:
threads:
virtual:
enabled: true这会自动将Tomcat的请求处理线程池替换为虚拟线程执行器。同时,@Async、@Scheduled方法也会默认使用虚拟线程。
自定义虚拟线程执行器(用于业务隔离):
@Configuration
public class VirtualThreadConfig {
@Bean
public Executor virtualTaskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
@Service
public class AsyncService {
@Async("virtualTaskExecutor")
public CompletableFuture<String> fetchData() {
// 阻塞调用外部API,虚拟线程自动处理
return CompletableFuture.completedFuture(restTemplate.getForObject("...", String.class));
}
}JDK 21提供了新的JVM接口:
jcmd <pid> Thread.dump_to_file -format=json threads.json输出中每个线程的threadType为VIRTUAL,并包含carrierThread字段显示当前载体线程ID。
编程式获取:
Thread.getAllStackTraces().keySet().stream()
.filter(t -> t.isVirtual())
.forEach(t -> System.out.println(t.getName() + " carrier: " + t.getCarrierThread()));虚拟线程的引入,标志着Java正式进入“海量并发”时代。它并非要替代平台线程,而是将阻塞型任务从稀缺资源中解放出来。对于Web服务、微服务、批处理等IO密集型场景,迁移成本极低,收益立竿见影。
核心公式:
系统吞吐量 ≈ 载体线程数 × (1 / 平均阻塞时间) 虚拟线程使得载体线程数≈CPU核数,而阻塞时间不再成为瓶颈。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。