简介
虚拟线程的英文名是 Virtual Thread,它是 Project Loom 带来的轻量级线程实现。
虚拟线程在 JDK 19、JDK 20 中经历了两轮预览,到了 JDK 21 正式发布。
简单理解:
1平台线程:Java 线程长期绑定操作系统线程 2虚拟线程:大量 Java 线程由 JVM 调度到少量操作系统线程上 3
传统 Java 服务经常采用“一请求一线程”的处理方式。
代码很直观,但平台线程数量有限。当大量请求都在等待数据库、HTTP 接口、文件或消息队列时,线程本身会先成为瓶颈。
虚拟线程让这种同步写法重新具备更高的并发承载能力:
1一个任务对应一个虚拟线程 2阻塞时暂时释放底层平台线程 3I/O 就绪后继续执行原来的代码 4
一句话概括:
1虚拟线程让阻塞式 Java 代码可以用更低的线程成本处理大量并发 I/O 任务。 2
版本要求
| JDK 版本 | 虚拟线程状态 |
|---|---|
| JDK 19 | 第一次预览 |
| JDK 20 | 第二次预览 |
| JDK 21 | 正式特性 |
| JDK 24+ | synchronized 场景下的线程固定问题得到大幅改善 |
最低运行版本:
1JDK 21 2
JDK 21 已经可以在生产项目中使用虚拟线程。
如果项目允许使用更新版本,JDK 24 及以上对虚拟线程的体验更完整,主要原因是 JEP 491 改进了 synchronized 与虚拟线程的配合方式。
平台线程为什么容易成为瓶颈
传统 Thread 默认创建的是平台线程。
1Thread thread = new Thread(() -> { 2 System.out.println("当前线程:" + Thread.currentThread()); 3}); 4 5thread.start(); 6thread.join(); 7
平台线程和操作系统线程大致是 1:1 的关系:
1Java 平台线程 1 -> OS 线程 1 2Java 平台线程 2 -> OS 线程 2 3Java 平台线程 3 -> OS 线程 3 4
平台线程需要操作系统参与创建、调度和上下文切换。
因此,平台线程通常会放进线程池重复使用:
1ExecutorService executor = Executors.newFixedThreadPool(200); 2
线程池可以降低反复创建线程的成本,但不会增加同时可用的平台线程数量。
假设线程池只有 200 个线程,每个任务都需要等待远程接口 1 秒,那么同一时间最多只有 200 个任务在执行,其余任务只能排队。
虚拟线程的工作方式
虚拟线程也是 java.lang.Thread 的实例,但不会在整个生命周期里固定占用某个操作系统线程。
大致关系如下:
1虚拟线程 1 --\ 2虚拟线程 2 ---\ 3虚拟线程 3 ----> 少量平台线程 -> 操作系统线程 4虚拟线程 4 ---/ 5虚拟线程 5 --/ 6
负责承载虚拟线程运行的平台线程,也叫载体线程,英文是 Carrier Thread。
当虚拟线程执行普通计算时,它会挂载到载体线程上。
当虚拟线程执行可感知的阻塞 I/O 时,JVM 可以暂停虚拟线程,并把载体线程交给其他任务使用。
1虚拟线程发起 I/O 2 | 3 v 4虚拟线程暂停并卸载 5 | 6 v 7载体线程执行其他虚拟线程 8 | 9 v 10I/O 完成 11 | 12 v 13虚拟线程重新挂载并继续执行 14
这个过程由 JVM 完成,业务代码仍然可以保持同步写法。
虚拟线程提升的是吞吐量
虚拟线程并不是运行速度更快的线程。
例如,一个接口调用本身需要 500 毫秒,换成虚拟线程后,这次调用通常仍然需要接近 500 毫秒。
变化主要体现在并发数量:
1单次任务耗时:通常不会明显降低 2同时处理的任务数:可以明显增加 3系统整体吞吐量:I/O 密集场景下可能提高 4
所以虚拟线程更适合:
- JDBC 数据库访问
- 同步 HTTP 调用
- RPC 调用
- 文件和网络 I/O
- 消息消费
- 大量并发请求
对于长时间占用 CPU 的计算任务,线程数量超过 CPU 核心数后,并不会因为使用虚拟线程而得到更高算力。
第一个虚拟线程 Demo
最简单的创建方式是 Thread.startVirtualThread。
1public class FirstVirtualThreadDemo { 2 3 public static void main(String[] args) throws InterruptedException { 4 Thread thread = Thread.startVirtualThread(() -> { 5 Thread current = Thread.currentThread(); 6 7 System.out.println("线程名称:" + current.getName()); 8 System.out.println("是否为虚拟线程:" + current.isVirtual()); 9 System.out.println("线程信息:" + current); 10 }); 11 12 thread.join(); 13 } 14} 15
输出类似:
1线程名称: 2是否为虚拟线程:true 3线程信息:VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1 4
join() 用来等待虚拟线程执行结束。
虚拟线程属于守护线程。如果主线程先结束,JVM 不会仅仅为了等待虚拟线程而继续运行,所以独立 Demo 中通常需要 join() 或执行器关闭等待。
使用 Thread.ofVirtual
Thread.ofVirtual() 可以设置线程名称,并选择立即启动或稍后启动。
立即启动:
1Thread thread = Thread.ofVirtual() 2 .name("order-query") 3 .start(() -> { 4 System.out.println(Thread.currentThread()); 5 }); 6 7thread.join(); 8
先创建再启动:
1Thread thread = Thread.ofVirtual() 2 .name("report-task") 3 .unstarted(() -> { 4 System.out.println("生成报表"); 5 }); 6 7thread.start(); 8thread.join(); 9
使用 ThreadFactory
需要统一线程名称时,可以创建虚拟线程工厂。
1import java.util.concurrent.ThreadFactory; 2 3public class VirtualThreadFactoryDemo { 4 5 public static void main(String[] args) throws InterruptedException { 6 ThreadFactory factory = Thread.ofVirtual() 7 .name("worker-", 0) 8 .factory(); 9 10 Thread first = factory.newThread(() -> printTask("任务 A")); 11 Thread second = factory.newThread(() -> printTask("任务 B")); 12 13 first.start(); 14 second.start(); 15 16 first.join(); 17 second.join(); 18 } 19 20 private static void printTask(String taskName) { 21 System.out.println(taskName + ",线程=" + Thread.currentThread().getName()); 22 } 23} 24
输出类似:
1任务 A,线程=worker-0 2任务 B,线程=worker-1 3
每个任务一个虚拟线程
业务代码里更常用的是:
1Executors.newVirtualThreadPerTaskExecutor() 2
它会为每个提交的任务创建一个新的虚拟线程。
1import java.util.concurrent.ExecutorService; 2import java.util.concurrent.Executors; 3 4public class VirtualThreadExecutorDemo { 5 6 public static void main(String[] args) { 7 try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { 8 for (int i = 1; i <= 5; i++) { 9 int taskId = i; 10 11 executor.submit(() -> { 12 try { 13 Thread.sleep(500); 14 System.out.printf( 15 "任务 %d 完成,线程=%s,virtual=%s%n", 16 taskId, 17 Thread.currentThread().getName(), 18 Thread.currentThread().isVirtual() 19 ); 20 } catch (InterruptedException e) { 21 Thread.currentThread().interrupt(); 22 } 23 }); 24 } 25 } 26 } 27} 28
ExecutorService 在这里不是传统意义上的固定大小线程池。
它的含义是:
1提交一个任务 2 | 3 v 4创建一个虚拟线程 5 | 6 v 7任务结束 8 | 9 v 10虚拟线程结束 11
虚拟线程创建成本低,不需要像平台线程那样池化复用。
一万个阻塞任务 Demo
下面的示例同时执行一万个休眠任务,用来观察虚拟线程处理大量阻塞任务的方式。
1import java.time.Duration; 2import java.time.Instant; 3import java.util.concurrent.ExecutorService; 4import java.util.concurrent.Executors; 5import java.util.stream.IntStream; 6 7public class ManyVirtualThreadsDemo { 8 9 public static void main(String[] args) { 10 int taskCount = 10_000; 11 Instant start = Instant.now(); 12 13 try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { 14 IntStream.range(0, taskCount).forEach(taskId -> 15 executor.submit(() -> { 16 try { 17 Thread.sleep(1000); 18 } catch (InterruptedException e) { 19 Thread.currentThread().interrupt(); 20 } 21 }) 22 ); 23 } 24 25 long millis = Duration.between(start, Instant.now()).toMillis(); 26 System.out.println("任务数:" + taskCount); 27 System.out.println("总耗时:" + millis + " ms"); 28 } 29} 30
这里的任务几乎不消耗 CPU,大部分时间都在等待。
因此,一万个任务可以同时处于等待状态,而不需要创建一万个操作系统线程。
这段代码适合观察机制,不适合作为严谨性能基准。正式压测还需要考虑 JVM 预热、连接池大小、下游容量、内存和监控开销。
Callable 和 Future
虚拟线程和普通 ExecutorService 一样支持 Callable、Future。
1import java.util.concurrent.ExecutorService; 2import java.util.concurrent.Executors; 3import java.util.concurrent.Future; 4 5public class VirtualThreadFutureDemo { 6 7 public static void main(String[] args) throws Exception { 8 try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { 9 Future<String> userFuture = executor.submit(() -> { 10 Thread.sleep(300); 11 return "用户信息"; 12 }); 13 14 Future<String> orderFuture = executor.submit(() -> { 15 Thread.sleep(500); 16 return "订单信息"; 17 }); 18 19 String result = userFuture.get() + " + " + orderFuture.get(); 20 System.out.println(result); 21 } 22 } 23} 24
两个任务可以并发等待,代码仍然保持从上往下的同步结构。
实战:并发聚合多个 HTTP 接口
下面使用 Java HttpClient 的同步 send 方法模拟接口聚合。
1import java.net.URI; 2import java.net.http.HttpClient; 3import java.net.http.HttpRequest; 4import java.net.http.HttpResponse; 5import java.time.Duration; 6import java.util.concurrent.ExecutorService; 7import java.util.concurrent.Executors; 8import java.util.concurrent.Future; 9 10public class HttpAggregationService { 11 12 private final HttpClient httpClient = HttpClient.newBuilder() 13 .connectTimeout(Duration.ofSeconds(2)) 14 .build(); 15 16 public UserPage loadUserPage(long userId) throws Exception { 17 try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { 18 Future<String> userFuture = executor.submit(() -> 19 get("http://localhost:8081/users/" + userId) 20 ); 21 22 Future<String> orderFuture = executor.submit(() -> 23 get("http://localhost:8082/orders?userId=" + userId) 24 ); 25 26 Future<String> accountFuture = executor.submit(() -> 27 get("http://localhost:8083/accounts/" + userId) 28 ); 29 30 return new UserPage( 31 userFuture.get(), 32 orderFuture.get(), 33 accountFuture.get() 34 ); 35 } 36 } 37 38 private String get(String url) throws Exception { 39 HttpRequest request = HttpRequest.newBuilder(URI.create(url)) 40 .timeout(Duration.ofSeconds(3)) 41 .GET() 42 .build(); 43 44 return httpClient.send( 45 request, 46 HttpResponse.BodyHandlers.ofString() 47 ).body(); 48 } 49 50 public record UserPage( 51 String user, 52 String orders, 53 String account 54 ) { 55 } 56} 57
三个远程调用之间没有依赖关系,所以可以分别放进虚拟线程并发执行。
异常和中断处理
虚拟线程仍然遵守 Java 线程的中断规则。
捕获 InterruptedException 后,常见处理方式是恢复中断标记:
1try { 2 Thread.sleep(1000); 3} catch (InterruptedException e) { 4 Thread.currentThread().interrupt(); 5 return; 6} 7
使用 Future 时,可以取消任务:
1Future<String> future = executor.submit(() -> loadRemoteData()); 2 3if (requestCancelled) { 4 future.cancel(true); 5} 6
cancel(true) 会尝试中断正在运行任务的线程。
业务代码需要正确响应中断,才能及时停止任务。
限制下游并发数量
虚拟线程数量可以很多,但数据库、远程服务、连接池和文件句柄仍然是有限资源。
比如某个第三方接口最多允许 20 个并发请求,可以使用 Semaphore 限流。
1import java.util.concurrent.Semaphore; 2 3public class LimitedRemoteClient { 4 5 private final Semaphore semaphore = new Semaphore(20); 6 7 public String call(String request) throws InterruptedException { 8 semaphore.acquire(); 9 10 try { 11 return callRemoteService(request); 12 } finally { 13 semaphore.release(); 14 } 15 } 16 17 private String callRemoteService(String request) throws InterruptedException { 18 Thread.sleep(200); 19 return "result:" + request; 20 } 21} 22
这里限制的是“同时访问下游的任务数”,不是虚拟线程总数。
简单关系:
1虚拟线程负责表达任务 2Semaphore 负责限制并发 3连接池负责限制数据库连接 4
虚拟线程和数据库连接池
开启虚拟线程后,数据库连接池仍然需要保留。
例如:
1同时有 5000 个虚拟线程查询数据库 2HikariCP 最大连接数是 30 3
同一时间仍然只有 30 个任务能够拿到数据库连接,其余任务会等待连接。
虚拟线程降低的是“等待线程”的成本,不会让数据库凭空增加处理能力。
连接池大小仍然需要结合以下因素设置:
- 数据库最大连接数
- SQL 执行时间
- 数据库 CPU 和磁盘能力
- 应用实例数量
- 事务持有连接的时间
虚拟线程和 ThreadLocal
虚拟线程支持 ThreadLocal。
请求上下文、事务信息、链路追踪 ID 等常见用法可以继续工作。
1private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>(); 2 3public void handle(String traceId) { 4 TRACE_ID.set(traceId); 5 6 try { 7 process(); 8 } finally { 9 TRACE_ID.remove(); 10 } 11} 12
需要关注的是另一类用法:把昂贵对象缓存到 ThreadLocal,希望多个任务反复复用。
虚拟线程通常一个任务使用一次。如果每个虚拟线程都创建一份大型对象,内存消耗会随着并发任务数增长。
因此,适合放入 ThreadLocal 的通常是轻量上下文,而不是连接、客户端或大型缓存对象。
虚拟线程和 synchronized
这一部分需要按 JDK 版本区分。
JDK 21 到 JDK 23
虚拟线程在 synchronized 代码块或同步方法内部发生阻塞时,可能被固定到载体线程。
1public synchronized String loadData() throws InterruptedException { 2 Thread.sleep(1000); 3 return "data"; 4} 5
如果大量虚拟线程长时间以这种方式阻塞,可用载体线程会减少,吞吐量可能下降。
JDK 24 及以上
JEP 491 改进了 JVM 的监视器实现。
虚拟线程在 synchronized 方法、同步代码块、等待监视器时,也可以释放载体线程。因 synchronized 导致的固定问题已基本消除。
因此,在 JDK 24 及以上版本中,不需要仅仅为了虚拟线程而把所有 synchronized 改成 ReentrantLock。
锁的选择可以重新回到代码语义:
1synchronized:语法简单,适合普通互斥 2ReentrantLock:适合超时、可中断、公平锁、多个 Condition 等高级需求 3
即使使用较新 JDK,锁范围仍然适合保持精简,长时间持锁执行 I/O 也容易造成业务层面的竞争。
仍可能发生固定的场景
JDK 24 以后仍有少量固定场景,主要和原生代码、Foreign Function 调用及部分 JVM 内部过程有关。
这类问题通常需要结合 JFR 和线程转储定位。
虚拟线程和 CPU 密集型任务
下面的任务一直占用 CPU:
1public long calculate() { 2 long result = 0; 3 4 for (long i = 0; i < 5_000_000_000L; i++) { 5 result += i; 6 } 7 8 return result; 9} 10
这种任务没有多少等待时间。
如果同时创建十万个虚拟线程执行计算,CPU 核心数量不会增加,反而会带来更多调度和竞争。
CPU 密集型任务通常仍然适合使用有界平台线程池:
1int processors = Runtime.getRuntime().availableProcessors(); 2ExecutorService cpuExecutor = Executors.newFixedThreadPool(processors); 3
可以用一句话区分:
1大量时间在等待 I/O:考虑虚拟线程 2大量时间在执行计算:考虑有界平台线程池 3
Spring Boot 开启虚拟线程
较新的 Spring Boot 项目可以通过配置开启虚拟线程:
1spring: 2 threads: 3 virtual: 4 enabled: true 5
Properties 写法:
1spring.threads.virtual.enabled=true 2
虚拟线程要求 Java 21 或更高版本。
开启后,Spring Boot 会在支持的自动配置位置使用虚拟线程,例如任务执行、Spring MVC 异步处理以及 Web 容器请求处理等场景。
如果项目自定义了 Executor、AsyncConfigurer 或 applicationTaskExecutor,需要确认自定义 Bean 是否覆盖了自动配置。
Spring Boot Web Demo
Maven 依赖:
1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4</dependency> 5 6<dependency> 7 <groupId>org.springframework.boot</groupId> 8 <artifactId>spring-boot-starter-jdbc</artifactId> 9</dependency> 10 11<dependency> 12 <groupId>com.mysql</groupId> 13 <artifactId>mysql-connector-j</artifactId> 14 <scope>runtime</scope> 15</dependency> 16
配置:
1spring: 2 threads: 3 virtual: 4 enabled: true 5 6 datasource: 7 url: jdbc:mysql://localhost:3306/virtual_thread_demo 8 username: root 9 password: 123456 10 hikari: 11 maximum-pool-size: 20 12 13server: 14 port: 8080 15
准备数据表:
1CREATE DATABASE virtual_thread_demo DEFAULT CHARACTER SET utf8mb4; 2 3USE virtual_thread_demo; 4 5CREATE TABLE users ( 6 id BIGINT PRIMARY KEY AUTO_INCREMENT, 7 username VARCHAR(50) NOT NULL, 8 email VARCHAR(100) NOT NULL, 9 status VARCHAR(20) NOT NULL 10); 11 12INSERT INTO users (username, email, status) VALUES 13('张三', 'zhangsan@example.com', 'ACTIVE'), 14('李四', 'lisi@example.com', 'ACTIVE'); 15
实体:
1public record User( 2 Long id, 3 String username, 4 String email, 5 String status 6) { 7} 8
Repository:
1import org.springframework.jdbc.core.JdbcTemplate; 2import org.springframework.stereotype.Repository; 3 4import java.util.Optional; 5 6@Repository 7public class UserRepository { 8 9 private final JdbcTemplate jdbcTemplate; 10 11 public UserRepository(JdbcTemplate jdbcTemplate) { 12 this.jdbcTemplate = jdbcTemplate; 13 } 14 15 public Optional<User> findById(Long id) { 16 String sql = """ 17 select id, username, email, status 18 from users 19 where id = ? 20 """; 21 22 return jdbcTemplate.query(sql, (rs, rowNum) -> new User( 23 rs.getLong("id"), 24 rs.getString("username"), 25 rs.getString("email"), 26 rs.getString("status") 27 ), id).stream().findFirst(); 28 } 29} 30
Service:
1import org.springframework.stereotype.Service; 2 3@Service 4public class UserService { 5 6 private final UserRepository userRepository; 7 8 public UserService(UserRepository userRepository) { 9 this.userRepository = userRepository; 10 } 11 12 public User findById(Long id) { 13 return userRepository.findById(id) 14 .orElseThrow(() -> new IllegalArgumentException("用户不存在,id=" + id)); 15 } 16} 17
Controller:
1import org.springframework.web.bind.annotation.GetMapping; 2import org.springframework.web.bind.annotation.PathVariable; 3import org.springframework.web.bind.annotation.RequestMapping; 4import org.springframework.web.bind.annotation.RestController; 5 6import java.util.Map; 7 8@RestController 9@RequestMapping("/api/users") 10public class UserController { 11 12 private final UserService userService; 13 14 public UserController(UserService userService) { 15 this.userService = userService; 16 } 17 18 @GetMapping("/{id}") 19 public Map<String, Object> findById(@PathVariable Long id) { 20 Thread thread = Thread.currentThread(); 21 User user = userService.findById(id); 22 23 return Map.of( 24 "thread", thread.toString(), 25 "virtual", thread.isVirtual(), 26 "user", user 27 ); 28 } 29} 30
访问:
1GET http://localhost:8080/api/users/1 2
响应示例:
1{ 2 "thread": "VirtualThread[#42,tomcat-handler-0]/runnable", 3 "virtual": true, 4 "user": { 5 "id": 1, 6 "username": "张三", 7 "email": "zhangsan@example.com", 8 "status": "ACTIVE" 9 } 10} 11
这套写法仍然是同步阻塞风格:
1Controller 2 | 3 v 4Service 5 | 6 v 7JdbcTemplate 8 | 9 v 10数据库 11
区别在于每个请求可以由独立虚拟线程处理,等待 JDBC 返回时不会长期占用稀缺的平台线程。
Spring @Async Demo
开启异步支持:
1import org.springframework.context.annotation.Configuration; 2import org.springframework.scheduling.annotation.EnableAsync; 3 4@Configuration 5@EnableAsync 6public class AsyncConfig { 7} 8
异步任务:
1import org.springframework.scheduling.annotation.Async; 2import org.springframework.stereotype.Service; 3 4import java.util.concurrent.CompletableFuture; 5 6@Service 7public class ReportService { 8 9 @Async 10 public CompletableFuture<String> generate(Long reportId) { 11 try { 12 Thread.sleep(1000); 13 14 String result = "报表生成完成,id=" + reportId 15 + ",virtual=" + Thread.currentThread().isVirtual(); 16 17 return CompletableFuture.completedFuture(result); 18 } catch (InterruptedException e) { 19 Thread.currentThread().interrupt(); 20 return CompletableFuture.failedFuture(e); 21 } 22 } 23} 24
在没有自定义执行器覆盖自动配置的情况下,启用 spring.threads.virtual.enabled=true 后,Spring Boot 自动配置的异步任务执行器会使用虚拟线程。
虚拟线程和 WebFlux 的区别
虚拟线程与 WebFlux 都能提高 I/O 密集型服务的并发能力,但编程模型不同。
| 对比项 | 虚拟线程 | Spring WebFlux |
|---|---|---|
| 编程风格 | 同步阻塞 | 响应式非阻塞 |
| 代码结构 | 普通方法、循环、try/catch | Mono、Flux、操作符链 |
| JDBC / JPA | 可以直接使用 | 会阻塞事件循环,需要隔离或使用 R2DBC |
| 调试方式 | 普通线程栈 | 响应式调用链 |
| 常见场景 | 传统 MVC、同步 SDK、JDBC | 流式响应、网关、全链路响应式系统 |
两者不是简单的替代关系。
普通 Spring MVC、JDBC、JPA、同步 HTTP 客户端项目,虚拟线程通常更容易接入。
SSE、背压、持续数据流、响应式数据源等场景,WebFlux 依然有明确价值。
线程池参数为什么可能失效
开启 Spring Boot 虚拟线程后,原先用于配置平台线程池大小的参数可能不再生效。
原因是虚拟线程不是在应用自己的固定线程池里反复复用,而是由 JVM 的全局虚拟线程调度器运行。
例如下面这类参数需要重新评估:
1核心线程数 2最大线程数 3线程空闲时间 4任务队列容量 5
虚拟线程场景下,更值得关注的是:
- 数据库连接池
- HTTP 连接池
- 下游并发限制
- 请求超时
- 内存使用
- CPU 使用率
- 任务积压和失败率
监控和排查
虚拟线程数量可能非常多,传统 jstack 平铺展示所有线程并不方便。
可以使用 jcmd 生成新的线程转储。
文本格式:
1jcmd <PID> Thread.dump_to_file -format=text thread-dump.txt 2
JSON 格式:
1jcmd <PID> Thread.dump_to_file -format=json thread-dump.json 2
JDK Flight Recorder 也可以记录虚拟线程相关事件。
启动时录制:
1java -XX:StartFlightRecording=filename=virtual-thread.jfr,duration=60s -jar app.jar 2
对于 JDK 21 到 JDK 23,还可以使用下面的参数排查因 synchronized 导致的固定:
1java -Djdk.tracePinnedThreads=full -jar app.jar 2
JDK 24 已通过 JEP 491 移除这项诊断参数的实际作用,因为 synchronized 不再是主要固定来源。较新版本更适合使用 JFR 观察剩余固定场景。
常见使用建议
一个任务对应一个虚拟线程
虚拟线程很轻,不需要创建固定大小的虚拟线程池。
推荐入口:
1Executors.newVirtualThreadPerTaskExecutor() 2
用专门机制限制资源并发
限制外部接口并发时使用 Semaphore,限制数据库并发时使用数据库连接池。
虚拟线程负责运行任务,资源组件负责控制容量。
为 I/O 设置超时
虚拟线程降低了等待成本,但无限等待仍然会积累任务和内存。
数据库、HTTP、RPC、消息处理都适合设置合理超时。
1HttpRequest request = HttpRequest.newBuilder(uri) 2 .timeout(Duration.ofSeconds(3)) 3 .GET() 4 .build(); 5
保留中断状态
1catch (InterruptedException e) { 2 Thread.currentThread().interrupt(); 3} 4
中断是取消任务、关闭应用和超时控制的重要协作机制。
压测需要覆盖真实下游
只用 Thread.sleep 能说明虚拟线程可承载大量等待任务,但不能代表真实业务性能。
正式验证至少要包含:
- 数据库连接池容量
- SQL 和索引性能
- HTTP 连接池配置
- 下游限流策略
- JVM 堆内存
- P95、P99 延迟
- 错误率和超时率
常用 API 汇总
| API | 作用 |
|---|---|
| Thread.startVirtualThread(task) | 创建并立即启动虚拟线程 |
| Thread.ofVirtual() | 创建虚拟线程构建器 |
| Thread.Builder.start(task) | 创建并启动线程 |
| Thread.Builder.unstarted(task) | 创建尚未启动的线程 |
| Thread.Builder.factory() | 创建线程工厂 |
| Thread.isVirtual() | 判断是否为虚拟线程 |
| Executors.newVirtualThreadPerTaskExecutor() | 每个任务创建一个虚拟线程 |
| Thread.join() | 等待线程结束 |
| Thread.interrupt() | 请求中断线程 |
| Future.cancel(true) | 取消任务并尝试中断线程 |
| Semaphore | 限制某项资源的并发访问量 |
总结
虚拟线程没有改变 Java 代码的基本写法。
普通同步代码仍然可以使用:
1方法调用 2循环 3try/catch 4JDBC 5同步 HTTP 客户端 6ThreadLocal 7
变化发生在线程实现层面:
1平台线程数量有限,需要池化复用 2虚拟线程成本更低,一个任务可以对应一个线程 3阻塞 I/O 时,虚拟线程可以释放底层载体线程 4
虚拟线程适合大量并发、I/O 等待明显的应用。
CPU 密集型任务、数据库容量、连接池大小、下游限流等问题不会因为虚拟线程自动消失。
更准确的使用方式是:用虚拟线程承载大量任务,再用连接池、信号量、超时和监控控制有限资源。
《Java 虚拟线程实战指南:从 Thread API 到 Spring Boot 高并发应用》 是转载文章,点击查看原文。
