后端与系统架构3514 字

有了虚拟线程,线程池还有必要存在吗

·全库 596 / 611

《有了虚拟线程,线程池还有必要存在吗》共 0.4 万字,通篇围绕 并发、上下文、吞吐、成本 展开,归在「后端与系统架构」这一层。

覆盖并发上下文吞吐成本场景量化

适合给 AI 系统搭底座、要管得住的人

目录骨架(8 节)
  1. 一. Java内部线程实现模式
  2. 二. 虚拟线程
  3. 三. 虚拟线程的使用
  4. 1.1 当前Java线程模型有什么问题吗?
  5. 1.2 一个IO密集型应用的例子
  6. 2.1 虚拟线程如何工作
  7. 2.2 虚拟线程和线程池的异同
  8. 什么情况需要保留线程池呢?

有了虚拟线程,线程池还有必要存在吗?

背景:

线程池可以重用固定数量的线程,避免了频繁创建和销毁线程的开销。然而,即使是线程池也无法完全解决上下文切换和资源消耗的问题,尤其是在高并发场景下。此外,大量的线程创建还可能导致OutOfMemoryError。

Java19引入了虚拟线程概念,java21新特性推出虚拟线程:

是轻量级线程(类似于 Go 中的 “协程(Goroutine)”),可以减少编写、维护和调度高吞吐量并发应用程序的工作量。

轻量级的线程在JVM的用户模式下被管理,而不是直接映射到操作系统的线程上。

当代码调用到阻塞操作时例如 IO、同步、Sleep等操作时,JVM 会自动把 Virtual Thread 从平台线程上卸载,平台线程就会去处理下一个虚拟线程,通过这种方式,提升了平台线程的利用率,让平台线程不再阻塞在等待上,从底层实现了少量平台线程就可以处理大量请求,提高了服务吞吐和 CPU 的利用率。

如图所示:

操作系统线程(OS Thread):由操作系统管理,是操作系统调度的基本单位。

平台线程(Platform Thread):传统方式使用的Java.Lang.Thread,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射。

虚拟线程(Virtual Thread):一种轻量级,由 JVM 管理的线程。对应的实例 java.lang.VirtualThread 这个类。

载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程。

一. Java内部线程实现模式

绿色线程(Green Thread):远古时期,Java使用绿色线程模式。这个模式下,多线程的调度和管理有JVM完成。绿色线程模式才作用M:1线程映射模型。这里就有一个问题,Java不能够规模化管理这种线程,也就无法充分发挥硬件性能。同样的实现绿色线程也是一件非常有挑战性的事情,因为它需要非常底层的支持才能够良好运行。随后Java移除了绿色线程,转而使用本地线程。这使得Java的线程执行比绿色线程更慢。

平台线程(Platform Thread):从Java 1.2开始从绿色线程切换到了平台线程模式(有些人称之为本地线程(Native Thread))。在操作系统的帮助下,JVM 得以控制平台线程。平台线程的执行效率很高,但是开启和关闭他们的资源消耗较大。这就是为什么我们现在要使用线程池。这个模型遵循着 1:1 线程映射,即一个Java线程映射到一个内核线程。当一个Java线程被创建时,相应的一个对应的核心线程也会被创建,用来执行线程代码。自此之后,平台线程模型的做法就延续到了今天。

1.1 当前Java线程模型有什么问题吗?

只是对于操作系统内核线程的一个简单包装,真正的线程调度,还是由操作系统完成;

因为线程的创建和销毁都需要系统内核完成,涉及用户态切换,资源消耗较大;

本地线程需要保存他们的调用栈在内存中,大概2MB~20MB的预留空间。如果你有4GB内存,如果每个线程占用20MB内存,那么你就只能创建大概200个线程;

因为本地线程是一种系统资源,加载一个新的本地线程大概需要1毫秒;

上下文切换代价昂贵,需要一个到内核的系统调用;

上面这些强制性的限制会限制线程创建的数量,同时会导致性能下降和过度的内存消耗。因为我们不能创建更多的线程;

我们不能通过增加更多的线程来增应用规模,因为上下文切换和内存占用的代价高昂;

1.2 一个IO密集型应用的例子

考虑一台16GB内存的网络服务器。对于每个服务请求,都分配一个不同的线程。我们假设每个线程需要20MB内存空间,那么这台机器可以支持800个线程。当前,后端的API一般使用REST/SOAP调用方式,例如数据库操作和API信息转发这些IO密集型操作。由此可见,后端服务的主要是IO密集型而不是CPU密集型。

接着假设一下,一个IO操作需要100毫秒,请求执行(IO密集型)需要100毫秒,以及返回结果也需要100毫秒。同时,当每秒有800个请求时,线程数得到了最大容量。

让我们来计算一下单个请求的CPU占用时间

CPU时间 = 请求准备时间 + 返回结果准备时间

= 0.1ms + 0.1ms

= 0.2ms

对于800个请求呢?

800个线程的请求时间= 800 * 0.2ms

= 160ms

受限于我们的内存容量,我们只能创建800个请求,也就导致了我们CPU使用率并不高

CPU使用率=160ms / 1000ms

= 16%

那么如何才能使CPU的利用率到达90%呢?

16% = 800个线程

90% = X个线程

X = 4500

但是我们当前因为内存的限制不能创建那么多的线程,除非我们能突破这个限制,拥有90G内存。

90G的内存是一个比较离谱的数字,所以说创建本地线程很明显不能充分利用硬件资源。

资料里没有你的场景?

把你手上的数据和想解决的那个环节发上来,我们按具体情况回,不复述方法论。

发布需求