导读:本期聚焦于冷风创作的《如何在Java中理解协程?Project Loom与虚拟线程(Virtual Threads)初探》,敬请观看详情。线程阻塞导致系统吞吐量上不去,这曾经是Java高并发场景下绕不开的难题。传统线程模型中一个线程对应一个操作系统线程,创建和上下文切换的成本极高,动辄上万的并发请求就会把JVM压垮。Project Loom带来的虚拟线程改变了这一切:它由JVM调度而非操作系统,阻塞时自动让出底层载体线程,用极低的内存占用支撑百万级并发。本文将从线程与协程的本质区别讲起,深入剖析虚拟线程的底层原理、挂起与恢复机制、与Kotlin协程的对比,并给出从创建线程池到迁移虚拟线程的完整代码示例,同时分析适用场景与常见误区,帮助你快速掌握这项Java并发领域的重大变革。

协程这个概念在Kotlin、Go等语言中已经流行多年,而Java长期依赖操作系统的线程模型来处理并发,导致高并发场景下资源消耗巨大、性能瓶颈明显。Project Loom正是为了解决这一问题而生,它引入的虚拟线程让Java开发者终于拥有了轻量级的协程式并发能力。本文将从原理到实践,带你全面理解虚拟线程的设计思想与使用方式。

如何在Java中理解协程?Project Loom与虚拟线程(Virtual Threads)初探

一、从线程到协程:为什么Java需要虚拟线程

要理解虚拟线程,首先需要明白传统Java线程的本质。java.lang.Thread在JDK 19之前与操作系统的内核线程是一一对应的关系,这种模型被称为平台线程模型。每创建一个平台线程,操作系统就要分配约1MB的栈空间,并且线程之间的上下文切换需要陷入内核态,代价高昂。这就解释了为什么在典型的Web服务器中,线程池大小通常只能设置几百到几千,一旦并发请求数超过线程池容量,请求就会排队甚至被拒绝。

协程的核心思想是把调度的控制权从操作系统收回到用户态。协程是一种用户态的轻量级执行单元,它可以在适当的时机主动挂起,让出执行权给其他协程,之后在条件满足时再恢复执行。由于挂起和恢复都发生在用户空间,不需要内核态切换,成本极低。Go语言的goroutine、Kotlin的coroutine都是这个思路的实现。

虚拟线程正是Java版本的协程实现。它由JVM管理和调度,初始栈空间只有几百字节,可以按需增长,堆内存中存储栈帧。一个普通的笔记本电脑轻松创建上百万个虚拟线程完全不成问题。更重要的是,虚拟线程的最大优势在于不需要改变编程模型:你仍然可以写同步阻塞式的代码,获得异步非阻塞的性能,这就是所谓的同步代码的编写体验、异步代码的运行效率。

二、虚拟线程的底层原理:挂起、恢复与调度

虚拟线程的实现依赖于两个关键组件:调度器和Continuation机制。JDK内部的调度器是一个基于ForkJoinPool的工作窃取式调度器,它负责把虚拟线程分配到数量有限的载体线程上运行。载体线程本身是普通的平台线程,一个载体线程可以先后承载成千上万个虚拟线程,它们之间的关系是类似多路复用的关系。

Continuation可以理解为可暂停、可恢复的执行上下文。当虚拟线程执行到阻塞操作(如Thread.sleep()、网络IO、Lock.lock())时,JVM不会让载体线程一起阻塞,而是把虚拟线程的整个调用栈作为堆对象保存起来,然后释放载体线程去执行其他虚拟线程。等阻塞操作完成后,调度器会重新拾取一个可用的载体线程,恢复之前保存的栈帧,让虚拟线程从挂起点继续执行。

这个过程对开发者完全透明。这也是虚拟线程与响应式编程框架(如Reactor、RxJava)最本质的区别:响应式框架要求你用回调或者链式操作符重写代码逻辑,学习曲线陡峭、调试困难;而虚拟线程让传统的阻塞式代码自动获得了非阻塞的行为,jstack看到的堆栈依然是清晰可读的同步调用链。需要注意的是,只有经过JDK改造的阻塞点才会触发挂起,如果在虚拟线程中调用JNI本地代码发生阻塞,载体线程仍会被一起阻塞,这是实践中要特别警惕的坑。

三、动手实践:虚拟线程的创建与使用

从JDK 21开始,虚拟线程已正式转正。最简单的创建方式是使用Thread.ofVirtual()静态工厂方法,也可以通过Executors.newVirtualThreadPerTaskExecutor()创建一个为每个任务分配一个虚拟线程的执行器。下面通过代码示例演示几种典型用法。

import java.time.Duration;
import java.util.concurrent.*;

public class VirtualThreadDemo {

    public static void main(String[] args) throws Exception {
        // 方式一:直接创建并启动虚拟线程
        Thread vt = Thread.ofVirtual()
                .name("my-virtual-thread")
                .start(() -> {
                    System.out.println("当前线程: " + Thread.currentThread());
                });
        vt.join();

        // 方式二:使用执行器,每个任务一个虚拟线程
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // 模拟一万个并发阻塞任务
            int taskCount = 10_000;
            long start = System.currentTimeMillis();
            for (int i = 0; i < taskCount; i++) {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofMillis(500)); // 阻塞不会占用载体线程
                    return "done";
                });
            }
            long cost = System.currentTimeMillis() - start;
            System.out.println("提交" + taskCount + "个任务耗时: " + cost + "ms");
        }
        // try-with-resources 会在关闭时等待所有任务完成
    }
}

上面的代码中,一万个任务每个都休眠500毫秒,如果用平台线程池(假设200个线程)执行,总耗时大约需要25秒以上;而用虚拟线程执行,理论上500多毫秒就能全部完成,因为休眠期间载体线程被释放去运行其他任务。这种吞吐量的提升在高IO密集型场景中尤为显著。

在实际的Spring Boot应用中,迁移也非常简单。Spring Boot从3.2版本开始支持虚拟线程,只需在配置文件中设置spring.threads.virtual.enabled=true,Tomcat处理请求的线程就会自动切换为虚拟线程,业务代码一行都不用改。下面是一个对比传统线程池和虚拟线程处理性能差异的测试片段:

import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.concurrent.*;

public class IoBenchmark {

    // 对比:平台线程池 vs 虚拟线程执行大量HTTP请求
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(3))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://ipipp.com"))
                .GET()
                .build();

        // 平台线程池:固定200线程
        ExecutorService pool = Executors.newFixedThreadPool(200);
        runBatch(client, request, pool, "平台线程池");

        // 虚拟线程执行器
        ExecutorService vtPool = Executors.newVirtualThreadPerTaskExecutor();
        runBatch(client, request, vtPool, "虚拟线程");
    }

    private static void runBatch(HttpClient client, HttpRequest request,
                                  ExecutorService pool, String label) throws Exception {
        long start = System.currentTimeMillis();
        var futures = new java.util.ArrayList<Future<?>>();
        for (int i = 0; i < 5000; i++) {
            futures.add(pool.submit(() -> {
                client.send(request, HttpResponse.BodyHandlers.discarding());
                return null;
            }));
        }
        for (var f : futures) f.get(); // 等待全部完成
        System.out.println(label + " 完成5000个请求耗时: "
                + (System.currentTimeMillis() - start) + "ms");
        pool.shutdown();
    }
}

四、使用建议与常见误区

虚拟线程虽好,但并非银弹,使用时需要遵守一些重要原则。首先,不要池化虚拟线程。虚拟线程的设计初衷就是廉价、用完即弃,每次任务直接创建新的虚拟线程即可,复用反而会破坏其调度特性,这也是为什么推荐使用newVirtualThreadPerTaskExecutor而非固定大小的虚拟线程池。

其次要警惕针住问题。当虚拟线程在载体线程上执行synchronized同步块并发生阻塞时,JDK 21中载体线程无法被释放,会被一起阻塞。如果一个系统中大量虚拟线程都挤在synchronized块内阻塞等待,吞吐量会急剧下降。解决办法是改用ReentrantLock替代synchronized,或者升级到JDK 24,官方已经在后续版本中优化了这一问题。

最后,虚拟线程适合IO密集型任务,如数据库查询、HTTP调用、文件读写等;而CPU密集型计算任务并不能从虚拟线程中获益,因为计算过程中不会发生挂起,载体线程始终被占用,此时虚拟线程和平台线程没有本质区别。此外,ThreadLocal的使用也要克制,百万级虚拟线程如果都持有较大的ThreadLocal对象,内存压力会非常大,可以考虑使用JDK 21提供的ScopedValue作为替代方案。掌握这些原则后,你就可以放心大胆地在生产环境中拥抱虚拟线程,用最熟悉的同步编程风格写出高吞吐的并发应用了。

Java协程虚拟线程Project Loom修改时间:2026-09-02 14:56:51

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260902/49001.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。