Skip to content

06 虚拟线程深入解读

虚拟线程(Virtual Thread)是 Java 21 引入的重磅特性,它让 Java 具备了"百万并发"的能力。本文深入解读虚拟线程的使用方式和底层实现原理。

一、为什么需要虚拟线程?

1.1 传统线程的痛点

在 Java 中,线程(Thread)一直是并发的基础单位。但传统线程有几个严重问题:

java
// 传统线程池
ExecutorService executor = Executors.newFixedThreadPool(100);

// 问题1:线程是重量级资源
// - 每个线程占用 1MB 左右的栈内存
// - 1000 个线程 = 1GB 内存
// - 创建/销毁线程需要 OS 调度

// 问题2:阻塞代价高
// 当线程执行 I/O 操作(数据库查询、HTTP 请求)时,线程会阻塞
// 但这个线程仍然占用内存和 OS 资源

传统线程的困境:

场景线程数内存占用效果
1000 用户聊天1000 线程~1GB内存爆炸
10000 请求/秒10000 线程~10GB直接 OOM
等待 I/O阻塞占用浪费资源效率低下

1.2 虚拟线程的出现

Java 21 引入了虚拟线程(Virtual Thread),专门解决 I/O 密集型场景的并发问题:

java
// 虚拟线程:轻量级、高并发
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

// 1000000 个虚拟线程 = 1GB 内存(理论上)
// 阻塞时不占用 OS 线程

虚拟线程的核心优势:

  • 轻量级:每个虚拟线程仅占用几 KB 内存
  • 高并发:支持数十万甚至百万级并发
  • 低成本阻塞:I/O 阻塞时释放载体线程

二、虚拟线程的使用方式

2.1 基本创建方式

java
public class VirtualThreadDemo {
    
    public static void main(String[] args) {
        // 方式1:Thread.ofVirtual() 创建
        Thread vt1 = Thread.ofVirtual().name("my-vt-1").start(() -> {
            System.out.println("虚拟线程运行中: " + Thread.currentThread());
        });
        
        // 方式2:Thread.startVirtualThread() 便捷方法
        Thread vt2 = Thread.startVirtualThread(() -> {
            System.out.println("快捷方式创建的虚拟线程");
        });
        
        // 方式3:Executors.newVirtualThreadPerTaskExecutor()
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        
        // 提交 10000 个任务
        for (int i = 0; i < 10000; i++) {
            final int taskId = i;
            executor.submit(() -> {
                // 模拟 I/O 操作
                try {
                    Thread.sleep(Duration.ofSeconds(1));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Task " + taskId + " 完成");
            });
        }
        
        executor.shutdown();
    }
}

2.2 与 ThreadPoolExecutor 对比

java
public class ThreadComparison {
    
    // 传统线程池
    private static void traditionalThreadPool() {
        ExecutorService executor = Executors.newFixedThreadPool(100);
        
        for (int i = 0; i < 1000; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    // 模拟 I/O 等待
                    Thread.sleep(100);
                } catch (InterruptedException e) {}
                System.out.println("Task " + taskId);
            });
        }
        
        executor.shutdown();
    }
    
    // 虚拟线程
    private static void virtualThreadPool() {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        
        for (int i = 0; i < 1000; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    // 模拟 I/O 等待
                    Thread.sleep(100);
                } catch (InterruptedException e) {}
                System.out.println("Task " + taskId);
            });
        }
        
        executor.shutdown();
    }
}

性能对比:

指标传统线程池 (100)虚拟线程 (1000)
内存占用~500MB~50MB
吞吐量100 req/s10000 req/s
线程切换高开销无开销

2.3 在 Spring Boot 中使用

java
@Configuration
public class VirtualThreadConfig {
    
    @Bean
    public Executor virtualThreadExecutor() {
        // Spring Boot 3.2+ 支持
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

// 使用示例
@Service
public class AsyncService {
    
    @Async  // 自动使用虚拟线程(Spring Boot 3.2+)
    public void processAsync() {
        // 异步执行
    }
    
    // 或者手动使用
    @Autowired
    private Executor virtualThreadExecutor;
    
    public void manualVirtualThread() {
        virtualThreadExecutor.execute(() -> {
            // 虚拟线程执行
        });
    }
}

2.4 虚拟线程的局限性

java
public class VirtualThreadLimitations {
    
    public static void main(String[] args) {
        
        // 不适合 CPU 密集型任务
        ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor();
        vt.submit(() -> {
            // 虚拟线程不适合 CPU 密集型!
            // 因为虚拟线程需要载体线程执行,CPU 密集会占满载体线程
            long sum = 0;
            for (long i = 0; i < 1_000_000_000L; i++) {
                sum += i;
            }
            return sum;
        });
        
        // 适合 I/O 密集型任务
        vt.submit(() -> {
            // 数据库查询
            // HTTP 请求
            // 文件读写
            // 虚拟线程释放载体线程,其他虚拟线程可以使用
        });
        
        // 注意:虚拟线程不能使用 ThreadLocal
        // ThreadLocal.withInitial(() -> "value"); // 不适用
        
        // 但可以使用 ScopedValue(Java 21 新特性)
        static final ScopedValue<String> USER_INFO = ScopedValue.newInstance();
        
        ScopedValue.where(USER_INFO, "user123").run(() -> {
            // 在这个作用域内可以获取
            System.out.println(USER_INFO.get());
        });
    }
}

三、虚拟线程的底层原理

3.1 核心概念:载体线程

虚拟线程的原理可以理解为"不绑定 OS 线程的轻量级任务":

关键点:

  • 虚拟线程(Virtual Thread, VT)是用户态线程
  • 载体线程(Carrier Thread)是实际的 OS 线程
  • 多个 VT 共享少量载体线程
  • VT 阻塞时,载体线程被释放给其他 VT 使用

3.2 Continuation(延续)

虚拟线程的核心实现基于 Continuation(延续)

java
// Continuation 伪代码实现
public class Continuation {
    private final Stack stack;  // 栈帧数据
    private State state;        // RUNNING / SUSPENDED
    
    // 暂停(yield)
    public static void yield() {
        // 1. 保存当前栈帧到堆
        saveStack(currentThread.continuation);
        
        // 2. 切换到调度器
        switchToScheduler();
    }
    
    // 恢复(resume)
    public static void resume(Continuation cont) {
        // 1. 从堆恢复栈帧
        restoreStack(cont.stack);
        
        // 2. 继续执行
        continueExecution(cont);
    }
}

Continuation 的作用:

3.3 调度器(Scheduler)

虚拟线程的调度器默认是 ForkJoinPool

java
// 虚拟线程的默认调度器
public class VirtualThreadScheduler {
    
    // ForkJoinPool 作为默认调度器
    private static final ForkJoinPool DEFAULT_SCHEDULER = 
        ForkJoinPool.commonPool();
    
    public static void main(String[] args) {
        // 查看默认调度器
        System.out.println(ForkJoinPool.commonPool());
        
        // 虚拟线程使用调度器
        Thread vt = Thread.startVirtualThread(() -> {
            // 实际上由 ForkJoinPool 的载体线程执行
        });
    }
}

调度流程:

3.4 内存模型

虚拟线程将栈内存从堆外移到堆内

java
public class StackMemoryDemo {
    
    public static void main(String[] args) throws Exception {
        
        // 传统线程栈:在 OS 分配,约 1MB/线程
        // 虚拟线程栈:在 JVM 堆分配,约几百 KB/线程
        
        // 模拟查看虚拟线程的栈大小
        Thread vt = Thread.startVirtualThread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
        });
        
        vt.join();
        
        // 虚拟线程栈内存实际上是动态的
        // - 初始:几百 KB
        // - 最大:可以根据需要扩展
    }
}

内存对比:

类型栈位置默认大小最大10万线程总内存
传统线程OS 堆外1MB1MB~1GB
虚拟线程JVM 堆内512KB1GB~50MB

3.5 阻塞检测与切换

虚拟线程能够"感知"阻塞操作并自动切换:

java
public class BlockingDetection {
    
    public static void main(String[] args) {
        Thread.startVirtualThread(() -> {
            // 这些操作会让虚拟线程让出载体线程
            
            // 1. Thread.sleep()
            try {
                Thread.sleep(1000);  // 阻塞 → yield → 释放载体
            } catch (InterruptedException e) {}
            
            // 2. Object.wait()
            synchronized (new Object()) {
                try {
                    new Object().wait();  // 阻塞
                } catch (InterruptedException e) {}
            }
            
            // 3. LockSupport.park()
            LockSupport.parkNanos(1000000000);  // 阻塞
            
            // 4. I/O 操作(底层使用上述机制)
            // new URL("...").openConnection().getInputStream();
            
            // 不阻塞的操作
            // - CAS 操作 (AtomicInteger)
            // - 纯计算
        });
    }
}

四、实战:虚拟线程最佳实践

4.1 适用场景

java
public class BestPractice {
    
    // 适合使用虚拟线程的场景
    
    // 1. HTTP 服务端
    @GetMapping("/api/users")
    public List<User> getUsers() {
        // 每个请求一个虚拟线程
        // 10000 并发请求 = 10000 虚拟线程 = 少量载体线程
        return userService.findAll();
    }
    
    // 2. 消息消费者
    @JmsListener(queue = "orders")
    public void processOrder(Order order) {
        // 每个消息一个虚拟线程
        orderProcessor.process(order);
    }
    
    // 3. 批量处理
    public void batchProcess(List<Item> items) {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            items.forEach(item -> 
                executor.submit(() -> processItem(item))
            );
        }
    }
    
    // 不适合使用虚拟线程的场景
    
    // 1. CPU 密集型计算
    public void cpuHeavy() {
        // 虚拟线程会被 CPU 密集任务占满载体线程
        // 应该用传统线程池
    }
    
    // 2. 需要使用 ThreadLocal 的地方
    public void threadLocalUsage() {
        // 虚拟线程不支持 ThreadLocal
        // 改用 ScopedValue (Java 21+)
    }
}

4.2 性能调优

java
public class PerformanceTuning {
    
    public static void main(String[] args) {
        
        // 1. 控制并发数(信号量)
        Semaphore semaphore = new Semaphore(10000);
        
        ExecutorService vt = Executors.newVirtualThreadPerTaskExecutor();
        
        for (int i = 0; i < 100000; i++) {
            semaphore.acquire();
            vt.submit(() -> {
                try {
                    // 业务逻辑
                } finally {
                    semaphore.release();
                }
            });
        }
        
        // 2. 避免创建过多虚拟线程
        // 虚拟线程虽然轻量,但也不是无限
        // 建议:并发数 = 10000 ~ 100000
        
        // 3. 使用结构化并发(Java 21)
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var future1 = scope.fork(() -> task1());
            var future2 = scope.fork(() -> task2());
            
            scope.join();
            scope.throwIfFailed();
            
            // 结果自动合并
        }
    }
}

4.3 调试技巧

java
public class DebugTips {
    
    public static void main(String[] args) {
        
        // 1. 查看虚拟线程
        Thread.getAllStackTraces().keySet().stream()
            .filter(Thread::isVirtual)
            .forEach(System.out::println);
        
        // 2. 虚拟线程命名
        Thread vt = Thread.ofVirtual()
            .name("worker-", 0)  // worker-0, worker-1, ...
            .start(() -> {});
        
        // 3. 调试时查看载体线程
        System.out.println(Thread.currentThread());  
        // 输出类似: VirtualThread[#30]/runnable@ForkJoinPool-1-worker-3
        
        // 表示:在 ForkJoinPool-1 的 worker-3 上运行的虚拟线程 #30
    }
}

五、总结

5.1 虚拟线程核心要点

特性说明
引入版本Java 21 (预览版 Java 19/20)
创建方式Thread.ofVirtual(), Executors.newVirtualThreadPerTaskExecutor()
适用场景I/O 密集型任务(HTTP、数据库、文件等)
不适用场景CPU 密集型任务、需要 ThreadLocal 的场景
内存模型栈在堆内,动态扩展
阻塞处理自动 yield 释放载体线程
调度器ForkJoinPool

虚拟线程是 Java 继泛型、Lambda 之后最重要的特性。它让 Java 具备了"百万并发"的能力,是构建高并发系统的利器。掌握虚拟线程,将成为 Java 开发者的必备技能。