深入解析JAVA虚拟线程
摘要:通过修改JAVA基础类库和jvm,在不改变现有阻塞型编程习惯情况下引入虚拟线程,轻松实现io密集型的百万并发web应用。虚拟线程非常轻量,随用随扔,可以认为和创建一个普通的对象没有什么区别。本文深入研究虚拟线程的产生背景,原理,源码,特别是探究了Continuation 和VirtualThread 核心类,以及虚拟线程对ThreadLocal,Sleep()方法和OkHttp等的影响,可以让大家全方位了解虚拟线程。
注意,为了方便理解,本文的代码都在源码的基础上做了精简,只保留关键代码。
前言
自2014年的JDK8以后,JAVA 又有了几个LTS版本,JDK11(2018年),JDK 17(2021年),JDK21 (2023年),但是很多开发者的态度是,"你发你任你发,坚持用我的 JDK 8",认为后续的版本没有哪一个像 JDK8式的创新,因此没必要去学和改变。
但是JAVA已经有30年的历史,1:1的线程模型已经显得落后,让JAVA的单机并发能力比不上一些后来的语言,在高并发场景下需要的硬件成本也很贵,虽然目前也有了比如Spring Webflux
这样的应用层的解决方案,但是需要搭配Project Reactor
来使用,地狱式的回调逻辑,指数增长的编程难度,很难推广使用。虚拟线程的正式引入让JAVA又焕发了第二春,让JDK21确实也是一个里程碑式的版本。
虚拟线程的使用起来却非常简单,下面举个例子来感受一下:
try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {for (int i = 0; i < 100_0000; i++) {virtualExecutor.execute(() -> {// 执行业务 逻辑});}
}
如果是使用了Spring Boot 3,通过以下2个配置:轻松实现使用启用虚拟线程。
-
配置Executor,让Spring默认线程池改成虚拟线程。
@Configuration @EnableAsync public class AsyncConfig {@Beanpublic Executor asyncExecutor() {// 替换掉 Spring 容器 中的默认线程池,每个 @Async 任务使用一个虚拟线程return Executors.newVirtualThreadPerTaskExecutor();} }
-
application.yml
server:servlet:thread:virtual: true # MVC 请求使用虚拟线程
那么有了虚拟线程,有什么好处?可以简单在自己的电脑上测试如下代码:
平台线程(原始的线程):
public class TestPlatformThreads {public static void main(String[] args) {var counter = new AtomicInteger();while (true) {new Thread(() -> {int count = counter.incrementAndGet();System.out.println("Thread count = " + count);LockSupport.park();}).start();}}
}我的电脑测试结果,可以看到,很快就抛出异常了Thread count = 4062
[2.220s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 2048k, guardsize: 16k, detached.
[2.220s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-4062"
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reachedat java.base/java.lang.Thread.start0(Native Method)
虚拟线程:
public class TestVirtualThreads {public static void main(String[] args) {var counter = new AtomicInteger();while (true) {Thread.startVirtualThread(() -> {int count = counter.incrementAndGet();System.out.println("Thread count = " + count);LockSupport.park();});}}
}Thread count = 3592031
Thread count = 3592034
Thread count = 3592039虚拟线程轻松打印到几百万。
可以看到,虚拟线程的优势非常显著,同时虚拟线程设计得十分巧妙,对于开发者一直以来的阻塞式编程习惯,不需要任何修改,甚至精简了线程池的使用,上手非常快,从硬件角度,高并发情况下可以让CPU的时间更多放在业务的执行上,效率有了明显得提升,对内存的要求也更低一些,可以节省设备的成本。
本文深入研究虚拟线程的背景,原理,源码从多个角度来让大家了解虚拟线程。
背景
历史回顾
在了解虚拟线程以前,我们需要对java的线程发展历史做一下回顾。
1995 – JDK 1.0
-
Java 语言一开始就内置了
Thread
类和Runnable
接口。 -
那时,Java 的最大卖点之一就是“内置多线程支持”,相比 C/C++ 需要依赖系统 API 来写多线程,Java 直接提供了跨平台的抽象。
-
这也是 Java slogan 里的 “Multithreaded” 来源之一。可以说,系统线程的包装,在当时是JAVA的特色。
早期实现(JDK 1.0 ~ JDK 1.2)
-
不同操作系统上的 JVM 使用的线程模型不一样,有些 JVM 用的是 green threads(用户态线程),有些用 native threads(系统线程)。
-
例如:Solaris 上的早期 JVM 用 green threads,Windows 上的用 native threads。
JDK 1.3 开始(2000 年左右)
-
HotSpot JVM 成为主流实现,彻底抛弃了 green threads,全面改为 1:1 的 native threads(Java Thread ↔ OS Thread)。
-
从那之后,“Java 线程”基本上就等价于“操作系统线程”。
JDK 19(2022, 预览)/ JDK 21(2023, 正式)
-
引入了 虚拟线程(Virtual Threads),算是对 20 多年前被放弃的 green threads 的“现代化回归版”,但实现方式更高效、更贴合当前 JVM 和硬件环境。
-
这时才引入了新的术语:
-
Platform Thread(旧的、直接对应 OS 的线程)
-
Virtual Thread(新的、轻量级的、JVM 调度的线程
-
-
此时虚拟线程和系统相同线程的映射关系为M:N
平台线程的不足
那 Platform Thread(平台线程,旧版线程方式)存在什么问题呢?
-
内存不足挑战
在Java中,每个Platform Thread(平台线程) 1:1 对应系统线程,默认情况下,jvm 分配给每个平台线程的虚拟机栈的大小是1M。也就是说,一台8g内存的设备,全部的内存用来创建平台线程,最多也只能生成 8192 条线程,设备的资源迅速就耗尽了。
-
CPU上下文切换挑战
即使我们加大了内存,比如升级到了几十个G的内存,看起来可以支持上万或者几十万的线程,但是大量创建的系统线程,会导致CPU大量的时间用在线程的上下文切换中,浪费了CPU的性能,真正执行业务的时间变少。
平台线程如何应对大量并发
在JAVA web应用开发中,如何应对高并发请求呢?
-
使用线程池
-
在Spring MVC中,常用的容器 Servlet 容器(Tomcat/Jetty)的线程池,工作线程的数量默认是200,请求数量超出部分在队列中等待。
-
本质上是控制并发的数量
-
如果想提升并发,需要堆机器
-
-
直接使用中间件Netty
-
Netty 的本质是一套高性能网络通信框架,用少量线程支撑大量连接。
-
核心原理就是基于 Reactor 模式,通过事件循环(EventLoop)和非阻塞 I/O,把所有网络事件用少量线程驱动,从而实现高并发和高吞吐。
-
但是Netty 更偏向底层,如果完整应用是一个房子,Netty 只提供了地基,编写业务难度极大。
-
-
使用框架 Spring Webflux
特点
-
使用 Reactive Streams 规范(
Publisher
/Subscriber
/Subscription
/Processor
)。 -
基于 Project Reactor 实现
Flux
和Mono
,表示异步流。 -
非阻塞:一个线程可以同时处理成千上万个请求。
-
背压(Backpressure):消费者可以告诉生产者“别发太快”,避免内存爆炸。
-
高吞吐、低延迟:尤其在 IO 密集型应用(HTTP 调用、DB 查询、消息队列)中表现突出。
-
推广难点
- 要写
Mono
/Flux
,理解flatMap
/switchIfEmpty
/zip
等操作符,学习成本高-
需要转变以前的阻塞式编写习惯,在复杂业务条件下需要编写大量了回调式代码,地狱级体验。
-
大量第三方库(数据库驱动、SDK、客户端 API)是 阻塞式的。WebFlux 要求 端到端异步,否则阻塞点会拖垮性能。例如:JDBC 是阻塞式 → 必须用 R2DBC 才能全异步。很多老的 HTTP Client、Redis、Kafka 驱动都是阻塞式的 → 需要替换。
-
迁移成本非常高。
-
关于 Spring Webflux,我想多讲几句,它是Spring团队2017推出的基于Platform Thread上的解决方案,可以看到他们一直想推广这个解决方案,在Spring boot很多场景中已经引入了很多Flux
和 Mono
,为全面转向Spring Webflux 做铺垫,但是因为需要修改编程习惯,而且需要大量第三方库进行更新支持,推广难度非常大,使用起来非常拧巴,基本宣告了从应用层解决百万高并发的努力是徒劳的,是失败的。特别是GO语言已经指明了方向的情况下,说明JAVA需要更深层次的革命,也就是在JDK层面进行革新。
引入虚拟线程
虚拟线程(Virtual Threads)是在JDK 19 中首次引入的预览特性,在JDK 21 (长期支持版本,2023 年 9 月 19 日发布)正式引入,是作为 Project Loom(织布机) 的核心成果。领导者是 Ron Pressler(Oracle,主要贡献集中在 Java 并发、虚拟线程、结构化并发、Continuation API 等方向,是 JVM 的重要核心开发者之一)。其他参与者主要是 OpenJDK 社区的开发者们(包括来自 Oracle、Red Hat、SAP 等公司的工程师),从参与者就可以看出虚拟线程是一个基于基础类库和JVM的根正苗红的特性。 姗姗来迟来的虚拟线程,为JAVA语言注入了新的生机。
关于 Project Loom ,可以参考 https://developer.okta.com/blog/2022/08/26/state-of-java-project-loom
Project Loom aims to drastically reduce the effort of writing, maintaining, and observing high-throughput concurrent applications that make the best use of available hardware.
— Ron Pressler (Tech lead, Project Loom)
为什么说姗姗来迟呢?因为后来者GO语言在2009年发布的时候,直接支持了 Goroutine,而 java 语言类似虚拟线程的的概念,在9年后的Project Loom立项的时候才出现,经过了5年的开发和验证,在2023年引入JDK 21 ,才终于让JAVA赶上了GO语言的并发能力。
原理
无论是GO语言 ,还是 Spring Webflux,都说明了使用少量的系统线程完成百万