首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从Project Loom到Virtual Threads:Java高并发编程的范式革命与性能实测

从Project Loom到Virtual Threads:Java高并发编程的范式革命与性能实测

原创
作者头像
用户12608867
发布2026-08-19 11:52:26
发布2026-08-19 11:52:26
1130
举报

从Project Loom到Virtual Threads:Java高并发编程的范式革命与性能实测

当十万并发不再是理论值,而成为日常基准——JDK 21虚拟线程正在重写Java服务端的容量公式。

一、被“重量级”束缚的二十年

Java自1.0起便将线程直接映射为操作系统内核线程(1:1模型)。这种设计的代价极其明确:

  • 栈内存固定:每个线程默认分配1MB(HotSpot)~2MB(某些平台)的栈空间,创建10 000个线程即吃掉10GB堆外内存。
  • 上下文切换昂贵:CPU在千级线程间切换时,需要保存/恢复寄存器、程序计数器、TLB刷新,实测单机线程数超过5 000后,吞吐量呈断崖式下降。
  • 阻塞即浪费:当线程执行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个任务,对比平台线程与虚拟线程的完成时间。

3.1 环境准备(JDK 21+)

代码语言:javascript
复制
// build.gradle (或Maven)
plugins {
    id 'java'
}
sourceCompatibility = '21'
repositories {
    mavenCentral()
}

3.2 基准测试类

代码语言:javascript
复制
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机器)

  • Platform Thread (500固定):4123 ms
  • Virtual Thread (per-task):28 ms
  • Virtual Thread (限流200):10012 ms

解读:虚拟线程开启后,10万次阻塞等待几乎同时发起,载体线程(数量等于CPU核心数,如4~8个)反复挂载/卸载虚拟线程,确保CPU始终100%忙碌。而平台线程池即使开到500,仍有99.5%的任务排队等待空闲线程,吞吐差距高达147倍

四、深入原理:调度器与挂载机制

虚拟线程的调度器是ForkJoinPool的一个特殊实例(VirtualThreadScheduler),并行度默认等于Runtime.availableProcessors(),可通过-Djdk.virtualThreadScheduler.parallelism=N调整。

关键源码片段(JDK 21内部)

代码语言:javascript
复制
// 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会被触发,执行以下步骤:

  1. 冻结栈:遍历当前栈帧,将每个帧的局部变量、操作数栈、程序计数器保存到StackChunk对象(堆内)。
  2. 释放载体线程:载体线程从当前虚拟线程解绑,返回调度器队列,立即处理下一个就绪虚拟线程。
  3. 阻塞事件注册:将虚拟线程注册到事件完成器(如NioSocketImplSelector),待I/O就绪或超时后,调用Continuation.run()恢复。

此机制带来的红利:阻塞操作变得“便宜”,开发者可以随意使用Thread.sleep()ReentrantLockBlockingQueue.take(),而无需担心线程资源枯竭。

五、避坑指南:虚拟线程并非银弹

尽管虚拟线程强大,以下场景不推荐或无效

5.1 纯CPU密集型计算

虚拟线程无法提升算力,反而因频繁挂载/卸载增加开销。应保持平台线程数≈CPU核心数。

代码语言:javascript
复制
// 反例:计算密集型使用虚拟线程
Runnable cpuBound = () -> {
    BigInteger.probablePrime(2048, new Random()); // 高CPU消耗
};
// 应使用 newFixedThreadPool(cores)

5.2 同步块(synchronized)中的阻塞

synchronized块内包含阻塞操作,虚拟线程会将整个载体线程一起阻塞(因为synchronized由monitor实现,无法yield)。推荐替换为ReentrantLock,它支持Condition.await()可挂起虚拟线程而不阻塞载体。

代码语言:javascript
复制
// 坏实践
synchronized(lock) {
    Thread.sleep(1000); // 载体线程会被阻塞
}

// 好实践
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 阻塞操作,虚拟线程会yield,载体释放
    Thread.sleep(1000);
} finally {
    lock.unlock();
}

5.3 线程局部变量(ThreadLocal)

虚拟线程数量极大,若每个线程都持有ThreadLocal对象,内存泄漏风险暴增。推荐使用作用域局部变量或传递上下文参数。

5.4 池化虚拟线程

Executors.newVirtualThreadPerTaskExecutor()每次提交都创建新虚拟线程,无需池化。若使用Executors.newFixedThreadPool包装虚拟线程,则毫无意义。

六、生产级实践:Spring Boot 3.2 + 虚拟线程

Spring Boot 3.2已提供内置支持,只需在application.yml中开启:

代码语言:javascript
复制
spring:
  threads:
    virtual:
      enabled: true

这会自动将Tomcat的请求处理线程池替换为虚拟线程执行器。同时,@Async@Scheduled方法也会默认使用虚拟线程。

自定义虚拟线程执行器(用于业务隔离)

代码语言:javascript
复制
@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接口:

代码语言:javascript
复制
jcmd <pid> Thread.dump_to_file -format=json threads.json

输出中每个线程的threadTypeVIRTUAL,并包含carrierThread字段显示当前载体线程ID。

编程式获取:

代码语言:javascript
复制
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 删除。

目录
  • 从Project Loom到Virtual Threads:Java高并发编程的范式革命与性能实测
    • 一、被“重量级”束缚的二十年
    • 二、虚拟线程不是“轻量级线程”那么简单
    • 三、代码实战:百万任务并发下的性能碾压
      • 3.1 环境准备(JDK 21+)
      • 3.2 基准测试类
    • 四、深入原理:调度器与挂载机制
    • 五、避坑指南:虚拟线程并非银弹
      • 5.1 纯CPU密集型计算
      • 5.2 同步块(synchronized)中的阻塞
      • 5.3 线程局部变量(ThreadLocal)
      • 5.4 池化虚拟线程
    • 六、生产级实践:Spring Boot 3.2 + 虚拟线程
    • 七、性能监控:如何观察虚拟线程状态
    • 八、总结:重新定义并发编程的上限
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档