Java 虚拟线程实战指南:从 Thread API 到 Spring Boot 高并发应用

作者:唐青枫日期:2026/6/26

简介

虚拟线程的英文名是 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 一样支持 CallableFuture

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 容器请求处理等场景。

如果项目自定义了 ExecutorAsyncConfigurerapplicationTaskExecutor,需要确认自定义 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/catchMono、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 高并发应用》 是转载文章,点击查看原文


相关推荐


Java Flyway 实战指南:用 SQL 脚本管理数据库版本
唐青枫2026/6/17

简介 Flyway 是一个数据库迁移工具。 它解决的问题和 Liquibase 类似: 数据库结构怎么跟着项目版本一起演进。 不过 Flyway 的风格更简单直接。 它主要通过 SQL 文件管理数据库变更。 比如: V1__create_users_table.sql V2__add_user_email_column.sql V3__create_orders_table.sql V4__insert_init_data.sql 应用启动或命令执行时,Flyway 会检查哪些脚本已经执行过


AI 代理只会在本地打转?我用 MCP 给它接上手脚,3 步接通第一个外部服务
大鹏AI教育2026/6/10

AI 代理只会在本地打转?我用 MCP 给它接上手脚,3 步接通第一个外部服务 先说结论:很多人觉得自己的 AI 代理"不够聪明",其实它不笨,是够不着外面的世界——能读本地文件、能跑命令,却连不上你的数据库、内部接口、第三方服务。我一开始也卡在这儿,把 MCP 跑通后才明白:问题从来不在模型,在它有没有"手脚"。 这篇我把给 OpenClaw 小龙虾(Claude Code 同款)接第一个 MCP 服务的过程讲一遍,连我踩的三个坑和边界判断一起给你。 1. 真问题:AI 写得出脚本,却发不出请


前端跨域完全指南:从 JSONP 到 Nginx 反向代理,一次性彻底搞懂
不会敲代码12026/6/2

前端跨域完全指南:从 JSONP 到 Nginx 反向代理,一次性彻底搞懂 同源策略是浏览器最坚实的护城河,而跨域方案就是一道道精心设计的城门。 前言 前后端分离开发早已成为标配。前端跑 localhost:5173,后端跑 localhost:3000,端口不同,跨域就来了。再加上调用第三方 API、对接合作商接口,跨域问题几乎是每个前端开发者的必修课。 这篇文章从「为什么会有跨域」出发,一次性梳理 JSONP、CORS、WebSocket、postMessage、Vite Proxy、N


详解MySQL事务(超详细版)
一条泥憨鱼2026/5/25

🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临) 🎬精选专栏:数据结构与算法,JavaSE ,苍穹外卖日记 前言: “事务(Transaction)”是数据库开发里非常重要的知识。 简单来说: 事务就是“一组操作,要么全部成功,要么全部失败”。 它主要用于: 转账 下订单 库存扣减 支付系统 多表更新 这些场景都不能只执行一半,否则数据就会出错。 一、为什么需要事务? 先看一个经典案例: 银行转账 假设: 张三账户:1000 元


OpenClaw梦境系统使用介绍
handsomestWei2026/5/3

OpenClaw梦境系统使用介绍 全文链接:OpenClaw梦境系统使用介绍 本文整理 OpenClaw 2.x / 2.5 路线上围绕 Dream Engine(梦境 / 记忆抽象系统) 的能力划分、工作流、指令与场景示例。安装方式、子命令与频道行为会随版本迭代变化,以当前环境 openclaw --help 与官方文档为准。 一、2.x 新功能概览 功能模块关键改进使用上的直接收益① Dream Engine(记忆 / 梦境系统)引入 Dream 概念:对原始 Memor


DeepSeek-V4-Pro 写代码到底行不行?我拿 GLM-5.1 跟它硬碰硬比了一轮
孟健AI编程2026/4/24

大家好,我是孟健。 DeepSeek-V4-Pro 发了,官方说代码能力大幅升级。这种话我听得多了,每次新模型发布都这么说。 但我确实好奇:V4 在写代码这件事上,到底有没有追上 GLM-5.1? GLM-5.1 是我日常写代码的主力模型,用了几个月了,它什么水平我心里有数。所以这次我不跑 benchmark,不拼跑分,就拿我实际工作中的四个场景,让两个模型正面硬刚。 四个场景:源码分析、功能实现、大文件拆分、项目架构分析。 最后再算笔账,看看成本谁更划算。 场景一:项目分析,分析 Claude


飞书机器人权限批量导入
itmanll2026/4/15

{ "scopes": { "tenant": [ "contact:contact.base:readonly", "im:app_feed_card:write", "im:biz_entity_tag_relation:read", "im:biz_entity_tag_relation:write", "im:chat", "im:chat.access_event.bot_p2p_chat:read",


云计算基础
**Cara**2026/4/7

1.数据中心 数据中心就是用稳定的电力、网络、机房环境,支撑服务器和存储,安全、可靠、不间断地跑业务、存数据。 1.简述 1.数据中心: IDC Internet Data Center(互联网数据中心) 从作用上来看,数据中心就是一个超大号的机房,里面有很多很多的服务器,专门对数据进行集中管理(存储、计算、交换) 定义:一套复杂的设施 能容纳多个服务器及通信设备 2.数据中心级别 3.数据中心选址 地理条件: 海拔高 气温低 非地震带 成本因素 电费 政策导


TRAE Friends@济南第4次活动:100+极客集结,2小时极限编程燃爆全场!
飞哥数智谈2026/3/30

飞哥数智谈,现居于济南,AI提效、AI编程实践者,AI·Spring 社群发起人,同时,担任 TRAE Friends 社区济南 Fellow,致力于AI 提效与AI编程落地,最近长期举办 openclaw 系列活动《养虾记》。 昨天(3.28),100多位 AI 爱好者带着电脑齐聚齐鲁软件园,大家现场编程、热烈讨论、共同路演,活动气氛空前热烈。 活动感受 说实话,活动的热度有点超出预期。 要知道,这次活动不是嘉宾分享会、也不是普通的交流会,而是一次要求每个人带着电脑,现场编程的路演型 Wo


51 与32 单片机LED控制详解
小郝 小郝2026/3/22

针对LED控制的不同方法,需要根据单片机架构进行区分。以下是按单片机种类分类的函数用法说明: 51系列单片机控制方法 1. 直接端口操作 // 传统51单片机(如AT89C51) #include <reg51.h> sbit LED = P1^0; // 定义P1.0引脚控制LED void main() { while(1) { LED = 0; // 点亮LED(共阳极接法) Delay_ms(500); LED =

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读