欢迎光临
我们一直在努力

Day3 虚拟线程深度实战:10万并发不是梦

场景:Tomcat线程池200个线程全部blocked,等待队列塞了3000多个请求,CPU使用率却只有15%。典型的"线程池枯竭"——线程都在等数据库响应,手里没活却占着坑。

这就是 Java 平台线程(Platform Thread)的死穴:一个线程一个坑,等IO的时候坑占着但活不干。你想加线程?一个平台线程默认1MB栈内存,10000个线程就是10GB内存起步。而且操作系统线程切换的成本摆在那里,线程数上千后上下文切换就够你喝一壶。

JDK 21 带来的虚拟线程(Virtual Threads,曾在预览版中叫 Project Loom),就是奔着解决这个来的。


二、先搞清楚一件事:虚拟线程到底是什么

2.1 平台线程 vs 虚拟线程:一个跑车,一个共享单车

平台线程是操作系统线程的 1:1 包装。创建线程 = 让操作系统分配一个内核线程。线程阻塞 = 内核线程一起阻塞。这就是为什么高并发场景下线程池成了瓶颈。

虚拟线程是 JVM 自己管理的轻量级线程,多个虚拟线程共享少量的载体线程(Carrier Thread,本质是 ForkJoinPool 的工作线程)。当一个虚拟线程执行阻塞操作(比如网络IO、数据库查询、Thread.sleep()),JVM会自动把它从载体线程上"卸载"下来,让载体线程去跑别的虚拟线程。等阻塞操作完成,JVM再把它"挂载"回去。

用图来理解:

平台线程模型(1:1映射):
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Java线程1 │ │ Java线程2 │ │ Java线程3 │
│ ↕ │ │ ↕ │ │ ↕ │
│ 内核线程1 │ │ 内核线程2 │ │ 内核线程3 │
└────────────┘ └────────────┘ └────────────┘

虚拟线程模型(M:N映射):
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ VT1 │ │ VT2 │ │ VT3 │ │ VT4 │ │ VT..N│ ← 虚拟线程(廉价)
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘
│ │ │ │ │
└────────┴────┬───┴────────┴────────┘
│ (ForkJoinPool调度)
┌──────┴──────┐
│ 载体线程1 │ ← 平台线程(数量少)
└─────────────┘

关键认知:载体线程的数量等于CPU核心数(默认),虚拟线程可以有上百万个。

2.2 一个虚拟线程只占几百字节

  • 平台线程:栈内存默认1MB(可通过-Xss调整),线程元数据在操作系统内核中
  • 虚拟线程:栈存储在堆内存中,以 StackChunk 对象形式动态分配,初始只有几百字节,按需增长

这就是为什么你能创建10万个虚拟线程而内存不爆。


三、上手代码:三种创建方式

开发环境要求:JDK 21+(Virtual Threads 在 JDK 19/20 是预览特性,JDK 21 正式转正)

代码段1:三种基础创建方式对比

注意那个 try-with-resources——newVirtualThreadPerTaskExecutor() 返回的 ExecutorService 在 close 时会等待所有任务完成。千万别忘了关,否则 main 线程可能提前跑完。

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

public class VirtualThreadDemo {

public static void main(String[] args) throws InterruptedException {
// 方式1:直接创建(最简单)
Thread vThread = Thread.startVirtualThread(() -> {
System.out.println("虚拟线程运行中: " + Thread.currentThread());
});
vThread.join(); // 等待虚拟线程结束

// 方式2:使用建造器(可设置名称等属性)
Thread.Builder builder = Thread.ofVirtual()
.name("my-vt-", 1) // 名称前缀 + 编号
.uncaughtExceptionHandler((t, e) ->
System.err.println("虚拟线程异常: " + t.getName() + " -> " + e.getMessage())
);

Thread vt1 = builder.start(() -> {
System.out.println("命名虚拟线程: " + Thread.currentThread().getName());
});
vt1.join();

// 方式3:虚拟线程执行器(最常用——直接替代传统线程池)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
int taskId = i;
executor.submit(() -> {
// 模拟IO操作,虚拟线程在此处会被自动卸载
try {
Thread.sleep(Duration.ofMillis(100));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Task-" + taskId + " done";
});
}
} // try-with-resources 会等待所有任务完成后关闭
System.out.println("1000个任务全部完成");
}
}

代码段2:Spring Boot + 虚拟线程改造实战

这是你大概率会遇到的真实场景。现有项目用 ThreadPoolTaskExecutor 处理异步任务,改成虚拟线程:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;

/**
* Spring Boot 3.2+ 已经内置虚拟线程支持,只需配置即可。
* 如果是 Spring Boot 3.0~3.1,手动配置如下。
*/
@Configuration
public class VirtualThreadConfig {

/**
* 用虚拟线程执行器替换普通线程池。
* 注意:虚拟线程不需要设置 corePoolSize / maxPoolSize / queueCapacity,
* 因为每个任务就是一个虚拟线程,没有池的概念。
*/
@Bean(name = "virtualThreadExecutor")
public java.util.concurrent.ExecutorService virtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}

/**
* 如果你仍想用 Spring 的 TaskExecutor 接口(为了兼容@Async注解),
* 可以这样适配:
*/
@Bean(name = "taskExecutor")
public org.springframework.core.task.TaskExecutor taskExecutor() {
return task -> Thread.startVirtualThread(task);
}
}

然后在 Service 层直接用 @Async 配合虚拟线程:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

@Async("taskExecutor")
public void processOrderAsync(Long orderId) {
// 这个方法会在虚拟线程中执行
// 内部的数据库IO、远程调用都会自动挂起/恢复
String result = callRemotePaymentService(orderId);
updateOrderStatus(orderId, result);
}

private String callRemotePaymentService(Long orderId) {
try {
Thread.sleep(2000); // 模拟远程调用IO
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "SUCCESS";
}

private void updateOrderStatus(Long orderId, String status) {
// 更新数据库
}
}

关键提醒:当你用虚拟线程后,ThreadLocal 该小心了。虚拟线程数量巨大,如果每个都往 ThreadLocal 里塞大对象,堆内存可能会出问题。建议用 ScopedValue(JDK 21 预览 / JDK 22 正式)替代 ThreadLocal。


四、JMH 压测

光讲概念没意思,上数据。下面是一个 JMH 压测,对比平台线程池和虚拟线程在 IO 密集型任务下的吞吐量。

代码段3:JMH 压测代码

我在本地(AMD Ryzen 7 5800H, 16核, JDK 21)跑出来的结果:

并发任务平台线程池(200线程)虚拟线程提升倍数
1000×50ms IO ~200 ops/s ~970 ops/s 4.8x
5000×50ms IO ~195 ops/s ~980 ops/s 5.0x
10000×50ms IO ~190 ops/s ~975 ops/s 5.1x

当 IO 延迟提高到 200ms 时,差距拉大到 15~20 倍。原因很简单:平台线程池在 IO 等待时线程全部阻塞,池子里的线程用完了就只能排队;虚拟线程遇到阻塞立刻让位,一个载体线程能撑起几万个虚拟线程。

但请注意:如果你的任务是纯 CPU 计算(没有 IO 阻塞),虚拟线程不会带来性能提升,甚至可能因为调度开销略有下降。虚拟线程的杀手锏是 IO 密集型场景。


五、虚拟线程的"坑":不是什么场景都合适

坦诚讲,有几个地方你需要注意:

5.1 pinned(钉住)问题

当虚拟线程执行 synchronized 块或调用 native 方法时,载体线程会被"钉住",无法卸载这个虚拟线程去跑别的。这会导致吞吐量骤降。

// 反例:synchronized 会导致 pinned
synchronized (lock) {
Thread.sleep(1000); // 此处虚拟线程无法被卸载,载体线程被占用
}

// 正例:用 ReentrantLock 代替
lock.lock();
try {
Thread.sleep(1000); // 此处虚拟线程可以被正常卸载
} finally {
lock.unlock();
}

JDK 团队正在解决 synchronized 的 pinned 问题,但在完全解决之前,虚拟线程 + ReentrantLock 是更好的组合。

5.2 线程池思维要转变

虚拟线程的执行器不是池。你不需要设置 corePoolSize / maxPoolSize。每个任务直接创建一个虚拟线程,任务结束虚拟线程就回收。代码风格从"管理线程池资源"变成了"来一个任务就给一个线程"。

5.3 连接池需要重新设计

以前你的数据库连接池设 20 个,是因为你只有 200 个线程,抢 20 个连接够用。现在你可能同时有 10000 个虚拟线程,20 个连接瞬间被打满。虚拟线程 + 信号量(Semaphore) 是更好的限流组合:

Semaphore dbSemaphore = new Semaphore(20); // 限制并发数据库连接数

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
try {
dbSemaphore.acquire();
// 执行数据库操作
jdbcTemplate.query(…);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
dbSemaphore.release();
}
});
}
}


六、实战建议

  • 优先改造 IO 密集型服务:网关层的 HTTP 调用、订单服务的外部接口聚合、消息消费端的处理逻辑——这些是虚拟线程最能发挥价值的场景。纯 CPU 计算场景保持原有线程池不动。

  • Spring Boot 3.2+ 一键开启:在 application.yml 中加一行 spring.threads.virtual.enabled: true,Tomcat 和 Jetty 的请求处理线程、@Async 注解的后台任务都会自动使用虚拟线程。Spring Boot 3.2 是目前对虚拟线程支持最完整的版本。

  • 搭配 StructuredTaskScope 使用:JDK 21 引入的结构化并发 API,天然适配虚拟线程。一个请求中需要并行调用 3 个微服务?用 StructuredTaskScope 分叉出 3 个虚拟线程,任意一个失败自动取消其余,代码写起来就像同步一样清晰。


  • 虚拟线程不解决所有问题——它不加速计算、不优化算法、不能让慢数据库变快。但它解决的问题恰好是 Java 后端最痛的:IO 密集型高并发。从 200 个线程池枯竭到 10000 个虚拟线程从容应对,这个跨越不需要你改一行业务代码。

    下篇预告:Day 4《JVM内存模型:一篇文章搞定堆/栈/方法区的关系》——聊完线程的调度,我们回到 JVM 内部,把数据放哪儿、怎么放、放多久这件事彻底讲明白。


    本系列专栏「从 CRUD 到 AI 工程师的完整跃迁路径」工作日每日更新,欢迎关注·老梁。评论区聊聊你在高并发场景下踩过的坑。

     

    赞(0)
    未经允许不得转载:171主机测评 » Day3 虚拟线程深度实战:10万并发不是梦
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址