Java终于开始引入虚拟线程(协程)了

Java的虚拟线程(JEP425)旨在解决多线程编程的复杂性和资源消耗问题,提供轻量级的线程实现以提高应用程序的吞吐量。虚拟线程采用M:N调度,允许多个虚拟线程在少量的平台线程上运行,降低了创建线程的成本。与平台线程相比,虚拟线程更易于管理和观测,适合处理大量并发的I/O密集型任务,而非CPU密集型任务。新的线程API使得开发者能够更容易地利用这一特性,提高代码的可读性和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

d0b973230f51a058a12d80ce28f6bf1c.gif

高并发、多线程一直是Java编程中的难点,也是面试题中的要点。Java开发者也一直在尝试使用多线程来解决应用服务器的并发问题。但是多线程并不容易,为此一个新的技术出现了,这就是虚拟线程。

传统多线程的痛点

但是编写多线程代码是非常不容易的,难以控制的执行顺序,共享变量的线程安全性,异常可观察性等等都是多线程编程的难点。

如果每个请求在请求的持续时间内都在一个线程中处理,那么为了提高应用程序的吞吐量,线程的数量必须随着吞吐量的增长而增长。不幸的是线程是稀缺资源,创建一个线程的代价是昂贵的,即使引入了池化技术也无法降低新线程的创建成本,而且 JDK 当前的线程实现将应用程序的吞吐量限制在远低于硬件可以支持的水平。

为此很多开发人员转向了异步编程,例如CompletableFuture或者现在正热的反应式框架。但是这些技术要么摆脱不了“回调地狱”,要么缺乏可观测性。

解决这些痛点、增强Java平台的和谐,实现每个请求使用独立线程(thread-per-request style)这种风格成为必要之举。能否实现一种“成本低廉”的虚拟线程来映射到系统线程以减少对系统线程的直接操作呢?思路应该是没问题的!于是Java社区发起了关于虚拟线程的JEP 425[1]提案。

虚拟线程

虚拟线程(virtual threads)应该非常廉价而且可以无需担心系统硬件资源被大量创建,并且不应该被池化。应该为每个应用程序任务创建一个新的虚拟线程。因此,大多数虚拟线程将是短暂的并且具有浅层调用堆栈,只执行单个 HTTP 客户端调用或单个 JDBC 查询。与之对应的平台线程(  Platform Threads,也就是现在传统的JVM线程 )是重量级且昂贵的,因此通常必须被池化。它们往往寿命长,有很深的调用堆栈,并且在许多任务之间共享。

总而言之,虚拟线程保留了与 Java 平台的设计相协调的、可靠的每请求线程样式,同时优化了硬件的利用。使用虚拟线程不需要学习新概念,甚至需要改掉现在操作多线程的习惯,使用更加容易上手的API、兼容以前的多线程设计、并且丝毫不会影响代码的拓展性。

平台线程和虚拟线程的不同

为了更好理解这一个设计,草案对这两种线程进行了比较。

现在的线程

现在每个java.lang.Thread都是一个平台线程,平台线程在底层操作系统线程上运行 Java 代码,并在代码的整个生命周期内捕获操作系统线程。平台线程数受限于 OS 线程数。

平台线程并不会因为加入虚拟线程而退出历史舞台。

未来的虚拟线程

虚拟线程是由 JDK 而不是操作系统提供的线程的轻量级实现。它们是用户模式线程的一种形式,在其他多线程语言中已经成功(比如Golang中的协程和Erlang中的进程)。虚拟线程采用 M:N 调度,其中大量 (M) 虚拟线程被调度为在较少数量 (N) 的 OS 线程上运行。JDK 的虚拟线程调度程序是一种ForkJoinPool工作窃取的机制,以 FIFO 模式运行。

我们可以很随意地创建10000个虚拟线程:

// 预览代码
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}

无需担心硬件资源是否扛得住,反过来如果你使用Executors.newCachedThreadPool()创建10000个平台线程,在大多数操作系统上很容易因资源不足而崩溃。

为吞吐量而设计

但是这里依然要说明一点,虚拟线程并非为了提升执行速度而设计。它并不比平台线程速度快,它们的存在是为了提供规模(更高的吞吐量),而不是速度(更低的延迟)。它们的数量可能比平台线程多得多,因此根据利特尔定律,它们可以实现更高吞吐量所需的更高并发性。

换句话说,虚拟线程可以显著提高应用程序吞吐量

  • 并发任务的数量很高(超过几千个),并且

  • 工作负载不受 CPU 限制,因为在这种情况下,拥有比处理器内核多得多的线程并不能提高吞吐量。

虚拟线程有助于提高传统服务器应用程序的吞吐量,正是因为此类应用程序包含大量并发任务,这些任务花费大量的时间等待。

增强可观测性

编写清晰的代码并不是全部。对正在运行的程序状态的清晰表示对于故障排除、维护和优化也很重要,JDK 长期以来一直提供调试、分析和监视线程的机制。在虚拟线程中也会增强代码的可观测性,让开发人员更好地调试代码。

新的线程API

为此增加了新的线程API设计,目前放出的部分如下:

  • Thread.Builder  线程构建器。

  • ThreadFactory  能批量构建相同特性的线程工厂。

  • Thread.ofVirtual()  创建一个虚拟线程。

  • Thread.ofPlatform() 创建一个平台线程。

  • Thread.startVirtualThread(Runnable) 一种创建然后启动虚拟线程的便捷方式。

  • Thread.isVirtual() 测试线程是否是虚拟线程。

还有很多就不一一演示了,有兴趣的自行去看JEP425

总结

协程在Java社区已经呼唤了很久了,现在终于有了实质性的动作,这是一个非常重要的特性。不过这个功能涉及的东西还是很多的,包括平台线程的兼容性、对ThreadLocal的一些影响、对JUC的影响。可能需要多次预览才能最终落地,不过这已经是很大的进步了,起码距离实装已经不远了,胖哥可能赶不上那个时候了,不过很多年轻的同学应该能够赶上。

参考资料

[1]

JEP 425: https://openjdk.java.net/jeps/425

3c4fa7c9ee8059d62b93ebccf5b34313.gif

### Go 协程Java 虚拟线程的工作原理及对比 #### 1. 基本概念 Go 的协程(goroutine)和 Java虚拟线程(Virtual Thread)都是为了提供一种高效的并发编程模型而设计的技术。 - **Go 协程** Goroutines 是由 Go 运行时管理的一种轻量级线程,它们运行在操作系统线程之上。Go 使用多路复用技术(multiplexing),将多个 goroutines 映射到少量的操作系统线程上,从而实现了高效率的上下文切换[^2]。 - **Java 虚拟线程** Java 虚拟线程是在 JDK 19 中引入的新特性,基于 Continuation 和 Fiber 技术实现。虚拟线程本质上是由 JVM 管理的轻量级线程,可以显著减少传统线程创建和销毁的成本。一个操作系统的线程能够承载成千上万个虚拟线程,极大地提高了资源利用率[^3]。 --- #### 2. 工作原理 - **Go 协程** 当启动一个新的 goroutine 时,Go 运行时会为其分配一个小栈空间(通常为几 KB)。这些 goroutines 并不会直接映射到操作系统线程,而是通过调度器动态地分配到可用的操作系统线程上。当某个 goroutine 阻塞时,Go 调度器会自动将其挂起并释放底层的操作系统线程给其他未阻塞的 goroutines 继续执行。 - **Java 虚拟线程** Java 虚拟线程的核心在于其使用了纤程(Fiber)的概念。每个虚拟线程都绑定在一个纤程上,而纤程本身并不依赖于操作系统线程。JVM 提供了一个专门的调度器来管理和分派这些虚拟线程至实际的操作系统线程中。如果某虚拟线程进入 I/O 或等待状态,则会被暂停并将对应的 OS 线程让渡给其他活跃的任务。 --- #### 3. 性能比较 - **内存占用** - Go 协程由于初始栈大小较小,默认只有几千字节,因此即使创建大量 goroutines,也不会消耗过多内存。 - Java 虚拟线程同样具有较低的内存开销,但由于其实现细节不同,在某些场景下可能稍逊于 Go 协程[^4]。 - **上下文切换成本** - Go 协程的设计使得它的上下文切换非常高效,尤其是在大规模并发情况下表现优异。实验表明,随着并发数量增加,Go 协程的总耗时几乎呈线性增长。 - Java 虚拟线程虽然也优化了上下文切换过程,但在极端条件下仍无法完全媲美 Go 协程的表现。 - **I/O 处理能力** - Go 的网络库原生支持异步非阻塞模式,这进一步增强了其处理海量连接的能力。 - Java 虚拟线程则利用 Project Loom 提供的功能简化了编写复杂的异步逻辑所需的代码复杂度。 --- #### 4. 应用场景推荐 - 如果目标环境主要涉及高并发短生命周期任务或者需要频繁进行 I/O 操作的应用开发,那么无论是选用 Go 协程还是 Java 虚拟线程都能带来明显优势。 - 对于已经熟悉生态体系的企业内部项目来说,采用各自语言自带工具链可能是更为稳妥的选择;而对于新创公司或个人开发者而言,可以根据具体需求灵活决定取舍。 ```java // 创建并启动一个 Java 虚拟线程示例 var thread = Thread.ofVirtual().start(() -> { System.out.println("Running on a virtual thread"); }); ``` ```go // 启动一个简单的 Go 协程 go func() { fmt.Println("This is running inside a goroutine.") }() ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小胖哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值