IWA
2025-09-25
点 赞
0
热 度
6
评 论
0

Java 虚拟线程(Virtual Threads)深度教程


—— Project Loom 带来的高并发新方式

一、为什么需要虚拟线程

1. 传统线程的限制

在 Java 中,线程是与操作系统线程(OS Thread)一一映射的:

  • 每个线程需要 1MB 左右的栈内存,成千上万个线程会占用巨量内存;

  • OS 线程切换需要 上下文切换,开销大;

  • 当线程因为 阻塞 I/O(例如网络请求、数据库访问)挂起时,它仍然占用一个宝贵的 OS 线程。

因此,传统线程适合 几百 ~ 几千 并发,而在 几万甚至几十万 并发场景下会遇到瓶颈。

2. 异步 / Reactive 模式的痛点

为解决并发问题,社区广泛采用了异步框架(如 CompletableFutureRxJavaReactorNetty 等)。
但异步模式带来 代码复杂度可读性差 的问题:

  • 回调地狱(callback hell);

  • 调试难度大,堆栈信息不直观;

  • 新手不易上手。

3. Project Loom 的目标

Project Loom 提供了 虚拟线程(Virtual Threads)

  • 由 JVM 管理的 轻量级线程

  • 一个 OS 线程可以挂载 / 调度成千上万个虚拟线程;

  • I/O 阻塞时,虚拟线程会挂起,OS 线程可去运行其他任务;

  • 允许我们用 同步阻塞代码风格 编写高并发程序。

一句话总结:让高并发像写同步代码一样简单


二、快速上手虚拟线程

1. 环境准备

  • JDK 21 或更高(LTS,支持虚拟线程)

  • 启动时需开启预览特性:

    javac --enable-preview --release 21 Main.java
    java --enable-preview Main
    

2. 创建虚拟线程

方式一:直接启动

Thread.startVirtualThread(() -> {
    System.out.println("Hello from virtual thread!");
});

方式二:ExecutorService

import java.util.concurrent.*;

public class VirtualThreadDemo {
    public static void main(String[] args) {
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> {
                System.out.println("Task running in " + Thread.currentThread());
            });
        }
    }
}

这里的 newVirtualThreadPerTaskExecutor() 相当于一个线程池,但为每个任务创建一个虚拟线程(几乎无成本)。


三、虚拟线程与平台线程对比

特性

平台线程(Platform Thread)

虚拟线程(Virtual Thread)

映射关系

一一映射 OS 线程

JVM 调度,映射在少量 OS 线程上

内存占用

~1MB/线程

几 KB ~ 数十 KB

并发能力

数千

数十万

阻塞 I/O

占用 OS 线程

自动挂起,释放 OS 线程

上下文切换

OS 调度,开销大

JVM 调度,开销小


四、实战案例:高并发 HTTP 请求

假设我们要并发执行 5000 个 HTTP 请求

传统线程池写法

ExecutorService pool = Executors.newFixedThreadPool(200);

for (int i = 0; i < 5000; i++) {
    pool.submit(() -> {
        fetch("https://httpbin.org/delay/1"); // 模拟耗时 I/O
    });
}

pool.shutdown();

缺点:

  • 线程数有限(200),其余任务要排队;

  • 提高线程池大小 → 内存暴涨。

虚拟线程写法

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

for (int i = 0; i < 5000; i++) {
    executor.submit(() -> {
        fetch("https://httpbin.org/delay/1");
    });
}

executor.shutdown();

特点:

  • 每个请求一个线程,代码同步直观;

  • JVM 自动调度,数千任务同时进行也没问题。

fetch() 方法实现示例:

static void fetch(String urlStr) {
    try {
        var url = new java.net.URL(urlStr);
        var conn = (java.net.HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.getInputStream().readAllBytes(); // 模拟读取
        System.out.println("Done: " + urlStr);
    } catch (Exception e) {
        System.err.println("Error: " + e);
    }
}

五、性能实验步骤

你可以在本机做对比实验:

  1. 实验条件

    • 测试任务数:5000

    • 每个任务耗时 ~1s(使用 httpbin.org/delay/1 模拟)

    • 对比:固定线程池(200 线程) vs 虚拟线程(5000 线程)

  2. 记录指标

    • 总耗时(任务完成时间)

    • CPU 使用率

    • 内存使用量

    • 平均延迟

  3. 预期结果

    • 固定线程池:任务分批执行,耗时 ~25s

    • 虚拟线程:所有任务并发执行,耗时 ~1~2s

    • 内存占用差异显著:平台线程几百 MB,虚拟线程几十 MB。


六、最佳实践

  1. 适用场景

    • I/O 密集型:数据库访问、HTTP 调用、RPC、文件 I/O。

    • 高并发服务:网关、聊天服务器、微服务。

  2. 不推荐场景

    • CPU 密集型任务(虚拟线程不会比平台线程更快)。

    • 使用某些 JNI 或依赖 OS 线程特性的库(需验证兼容性)。

  3. 与结构化并发结合
    Loom 还引入 Structured Concurrency 概念,可以用 StructuredTaskScope 来管理一组虚拟线程:

    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String> f1 = scope.fork(() -> fetch("https://a.com"));
        Future<String> f2 = scope.fork(() -> fetch("https://b.com"));
    
        scope.join(); // 等待任务完成
        scope.throwIfFailed();
    
        System.out.println(f1.resultNow() + f2.resultNow());
    }
    

    优点:让并发任务像代码块一样有生命周期,异常/超时能统一管理。


七、总结

  • 虚拟线程 = 高并发的新利器,解决了 OS 线程昂贵、异步代码复杂的问题。

  • 它让我们可以用 同步风格 写出 高并发、可读性高 的代码。

  • 最佳应用场景:I/O 密集型、高并发 Web 服务。

  • 在未来几年,虚拟线程有望成为 Java 服务端开发的“标配”。


用键盘敲击出的不只是字符,更是一段段生活的剪影、一个个心底的梦想。希望我的文字能像一束光,在您阅读的瞬间,照亮某个角落,带来一丝温暖与共鸣。

IWA

estp 企业家

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

文章目录

IWA的艺术编程,为您导航全站动态

26 文章数
9 分类数
10 评论数
26标签数
最近评论
IWA

IWA


👍

M丶Rock

M丶Rock


😂

M丶Rock

M丶Rock


感慨了

M丶Rock

M丶Rock


厉害了

M丶Rock

M丶Rock


6666666666666666666