虚拟线程快速体验

环境:JDK21 + IDEA

public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
}

运行上面的代码看下执行时间,再试下 Executors.newFixedThreadPool(20) 和 Executors.newCachedThreadPool()

不出意外的话,会发现Executors.newVirtualThreadPerTaskExecutor()运行速度最快,Executors.newCachedThreadPool()运行时系统最卡顿,Executors.newFixedThreadPool(20) 最慢。

Executors.newCachedThreadPool()卡顿是因为一个任务创建一个Platform线程,占用了太多系统资源。

Executors.newFixedThreadPool(20)运行慢是因为只有20个并发去执行1万个任务

Executors.newVirtualThreadPerTaskExecutor()类似Executors.newCachedThreadPool(),但是创建的是虚拟线程,所以在获得高并发的同时也没有占用太多系统资源。

为什么引入虚拟线程

首先,我们来看看现在的Java线程是怎样的。

java.lang.Thread 这个类我相信大家都不陌生,代表Java中的最小并发单元,即一个线程。它是Java对底层的操作系统线程(OS Thread)的封装,为了区别于OS线程,我们称之为平台线程(Platform Thread)。当我们初始化一个Thread实例时,其实就是创建了一个Platform线程并将之与一个OS线程绑定(1:1)。

这种方式存在以下问题:

  1. OS线程是有限的,Platform线程的创建数量受限制于OS线程
  2. 因为绑定系统资源,因此线程的创建/销毁的代价都是昂贵的

这两个问题并非无解,比如,问题1的本质是垂直扩展到顶了,完全可以用水平扩展的方式解决,一台机器的OS线程不能满足需求,再增加一台便是;问题2可以通过池化技术来解决,既然线程的创建和销毁代价比较昂贵,那便将创建好的线程收集起来,推迟销毁的时机,尽量复用它。

JDK21则是在语言层面上的提供了一个替代方案,也就是本文要介绍的虚拟线程(virtual thread),熟悉linux的同学肯定知道系统线程和用户线程的区别,虚拟线程就像是JDK实现的“用户线程”,下面来重点介绍。

什么是虚拟线程

虚拟线程,可以看作是对Platform线程的轻量级封装,Platform线程和OS线程的关系是1:1,虚拟线程和Platform线程的关系则是M:N,且一般M要远远大于N。

可以直接看下虚拟线程的构造函数源码加深理解,坐标java.lang.VirtualThread#。

虚拟线程实例化


final class VirtualThread extends BaseVirtualThread {
VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {
super(name, characteristics, /*bound*/ false);
Objects.requireNonNull(task); // choose scheduler if not specified
if (scheduler == null) {
Thread parent = Thread.currentThread();
if (parent instanceof VirtualThread vparent) {
scheduler = vparent.scheduler;
} else {
scheduler = DEFAULT_SCHEDULER;
}
} this.scheduler = scheduler;
this.cont = new VThreadContinuation(this, task);
this.runContinuation = this::runContinuation;
}
} private static ForkJoinPool createDefaultScheduler() {
ForkJoinWorkerThreadFactory factory = pool -> {
PrivilegedAction<ForkJoinWorkerThread> pa = () -> new CarrierThread(pool);
return AccessController.doPrivileged(pa);
};
PrivilegedAction<ForkJoinPool> pa = () -> {
int parallelism, maxPoolSize, minRunnable;
String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
String maxPoolSizeValue = System.getProperty("jdk.virtualThreadScheduler.maxPoolSize");
String minRunnableValue = System.getProperty("jdk.virtualThreadScheduler.minRunnable");
if (parallelismValue != null) {
parallelism = Integer.parseInt(parallelismValue);
} else {
parallelism = Runtime.getRuntime().availableProcessors();
}
if (maxPoolSizeValue != null) {
maxPoolSize = Integer.parseInt(maxPoolSizeValue);
parallelism = Integer.min(parallelism, maxPoolSize);
} else {
maxPoolSize = Integer.max(parallelism, 256);
}
if (minRunnableValue != null) {
minRunnable = Integer.parseInt(minRunnableValue);
} else {
minRunnable = Integer.max(parallelism / 2, 1);
}
Thread.UncaughtExceptionHandler handler = (t, e) -> { };
boolean asyncMode = true; // FIFO
return new ForkJoinPool(parallelism, factory, handler, asyncMode,
0, maxPoolSize, minRunnable, pool -> true, 30, SECONDS);
};
return AccessController.doPrivileged(pa);
}

可以看到,创建虚拟线程的时候,使用了一个默认的调度器(ForkJoinPool),也就是Platform的线程池,可以看到池子的几个配置参数。

  1. 最大Platform线程数:默认为系统核心数,最大为256,可以通过jdk.virtualThreadScheduler.maxPoolSize设置

这个时候,爱思考的同学可能就要问了,既然默认的最大Platform线程数为系统核心数,岂不是大大限制了并发能力?是不是要主动设置一个较大值?

答案是不需要,因为JDK在线程池的基础上实现了调度的功能。当虚拟线程启动时,调度器会将虚拟线程mount到Platform线程,此时该Platform线程被称为这个虚拟线程的carrier;当线程运行遇到IO操作需要等待时,调度器又会将虚拟现场unmount,把Platform线程释放出来给其他虚拟线程使用,不占用CPU时间。因此,对于非CPU密集的应用,很少的Platform线程就能支持大量的虚拟线程来执行任务。事实上,对于CPU密集的应用,虚拟线程并不会带来多大的提升。虚拟线程真正的应用场景是生存周期短、调用栈浅的任务,如一次http请求、一次JDBC查询。

需要明确的是,操作系统真正能同时运算的线程数也就只有逻辑CPU数,多出来的线程只能等待系统的调度获得CPU时间。

虚拟线程状态

stateDiagram-v2
NEW --> STARTED
STARTED --> TERMINATED
STARTED --> RUNNING
RUNNING --> TERMINATED

RUNNING --> PARKING
PARKING --> PARKED
PARKING --> PINNED
PARKED --> UNPARKED
PINNED --> RUNNING
UNPARKED --> RUNNING

可以看出,虚拟线程相较原先的线程状态,多了Parked、Unparked、Pinned等状态

  • Parked:就是前面说的mount

  • Unparked:就是前面说的unmount

  • Pinned:虚拟线程阻塞时,正常会unmount,但是在一些特殊场景下,不能unmount,此时就会进入Pinned状态:

    1. 阻塞操作在 synchronized 代码块中(后续JDK可能优化这一点限制)
    2. 执行 native 方法时

    Pinned状态占用了Platform线程,无疑会影响性能,官方建议对于经常执行的 synchronized 代码块,最好使用java.util.concurrent.locks.ReentrantLock 替代。如果不清楚自己代码里哪些地方使用到了 synchronized 代码块,在切换使用虚拟线程时,可以添加JVM参数jdk.tracePinnedThreads帮助排查。

总结

虚拟线程特别适用如下场景:有大量的并发任务需要执行,且任务是非CPU密集的。

虚拟线程使用上和普通的线程没有太大区别,甚至因为内置了调度逻辑和线程池,可以让开发人员不用再考虑线程池的大小、拒绝策略等,尤其给框架开发者提供了新的优化思路。

对于已经使用了reactive技术的如webFlux框架,没必要再切换到虚拟线程,两者性能相当。

对于web容器如tomcat来说,本身已经使用reactor、nio等技术优化吞吐量,在小的并发数场景下,没必要切换虚拟线程,提升不大。

一文揭开JDK21虚拟线程的神秘面纱的更多相关文章

  1. 揭开Sass和Compass的神秘面纱

    揭开Sass和Compass的神秘面纱 可能之前你像我一样,对Sass和Compass毫无所知,好一点儿的可能知道它们是用来作为CSS预处理的.那么,今天请跟我一起学习下Sass和Compass的一些 ...

  2. 揭开.NET消息循环的神秘面纱(GetMessage()无法取得任何消息,就会进入Idle(空闲)状态,进入睡眠状态(而不是Busy Waiting)。当消息队列不再为空的时候,程序会自动醒过来)

    揭开.NET消息循环的神秘面纱(-) http://hi.baidu.com/sakiwer/item/f17dc33274a04df2a9842866 曾经在Win32平台下奋战的程序员们想必记得, ...

  3. 揭开DRF序列化技术的神秘面纱

    在RESTful API中,接口返回的是JSON,JSON的内容对应的是数据库中的数据,DRF是通过序列化(Serialization)的技术,把数据模型转换为JSON的,反之,叫做反序列化(dese ...

  4. 揭开Vue异步组件的神秘面纱

    简介 在大型应用里,有些组件可能一开始并不显示,只有在特定条件下才会渲染,那么这种情况下该组件的资源其实不需要一开始就加载,完全可以在需要的时候再去请求,这也可以减少页面首次加载的资源体积,要在Vue ...

  5. 揭开redux,react-redux的神秘面纱

    16年开始使用react-redux,迄今也已两年多.这时候再来阅读和读懂redux/react-redux源码,虽已没有当初的新鲜感,但依然觉得略有收获.把要点简单写下来,一方面供感兴趣的读者参考, ...

  6. ASP.NET 运行时详解 揭开请求过程神秘面纱

    对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...

  7. java高级精讲之高并发抢红包~揭开Redis分布式集群与Lua神秘面纱

    java高级精讲之高并发抢红包~揭开Redis分布式集群与Lua神秘面纱 redis数据库 Redis企业集群高级应用精品教程[图灵学院] Redis权威指南 利用redis + lua解决抢红包高并 ...

  8. 揭开Future的神秘面纱——结果获取

    前言 在前面的两篇博文中,已经介绍利用FutureTask任务的执行流程,以及利用其实现的cancel方法取消任务的情况.本篇就来介绍下,线程任务的结果获取. 系列目录 揭开Future的神秘面纱—— ...

  9. 揭开Future的神秘面纱——任务执行

    前言 此文承接之前的博文 解开Future的神秘面纱之取消任务 补充一些任务执行的一些细节,并从全局介绍程序的运行情况. 系列目录 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱— ...

  10. 揭开Future的神秘面纱——任务取消

    系列目录: 揭开Future的神秘面纱——任务取消 揭开Future的神秘面纱——任务执行 揭开Future的神秘面纱——结果获取 使用案例 在之前写过的一篇随笔中已经提到了Future的应用场景和特 ...

随机推荐

  1. Windows Server系统 PaddleOCR失败解决方案

    因PaddleOCR引用了Opencv,在windows server 上 使用opencv出现 DLL load failed错误,发现缺失部分dll:MFPlat.dll.MF.dll.MFRea ...

  2. 机器学习策略篇:详解理解人的表现(Understanding human-level performance)

    理解人的表现 人类水平表现这个词在论文里经常随意使用,但现在告诉这个词更准确的定义,特别是使用人类水平表现这个词的定义,可以帮助推动机器学习项目的进展.还记得上个博客中,用过这个词"人类水平 ...

  3. Linux Shell命令提示样式修改

    对linux shell命令样式进行美化. 修改前的效果: 修改后的效果: 直接给出.bashrc脚本代码: 1 # ~/.bashrc: executed by bash(1) for non-lo ...

  4. Wgpu图文详解(01)窗口与基本渲染

    写在前面 如果对Rust与Wgpu比较关注的同学可能在网络上搜到过@sotrh国外大佬编写的<Learn Wgpu>,以及国内大佬@jinleili的优秀翻译作品<学习 Wgpu&g ...

  5. 阿里bxet逆向

    声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 目标网站 x82y 分析过 ...

  6. 算法学习笔记(45): 快速沃尔什变换 FWT

    遗憾的是 math 里面一直没有很好的讲这个东西--所以这次细致说说. FWT 的本质 类似于多项式卷积中,利用 ntt 变换使得卷积 \(\to\) 点乘,fwt 也是类似的应用. 定义某种位运算 ...

  7. SRE心里话:要求100%服务可用性就是老板的无知

    <SRE Google 运维解密>第3章讲了拥抱风险,一些关键的观点,在这里与大家分享,融入了我自己的一些理解,希望对你有些帮助. 服务可用性必须100%?其实完全没必要 一个服务客户的产 ...

  8. Scrapy框架(七)--中间件及Selenium应用

    中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. 作用:批量拦截到整个工程中所有的请求和响应 - 拦截请求: - UA伪装:proces ...

  9. rsync备份

    备份工具rsync 备份是太常见.且太重要的一个日常工作了. 备份源码.文档.数据库.等等. 类似cp命令拷贝,但是支持服务器之间的网络拷贝,且保证安全性. 学习背景 超哥游戏公司要每天都要对代码备份 ...

  10. Docker入门系列之三:十二个Dockerfile指令

    本篇文章是关于Dockerfiles的,这是Docker系列文章的第三部分.如果您还没有读过第一部分,请先阅读它,您可以从全新的角度了解Docker容器概念. 第二部分是Docker生态系统的简要介绍 ...