虚拟线程快速体验

环境: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. ASP.NET Core SignalR 概述

    什么是 SignalR? ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能. 实时 Web 功能使服务器端代码能够将内容推送到客户端. 适合 Sig ...

  2. weinre  远程实时调试手机上的Web页面 JAVASCRIPT远程调试

    版权归作者所有,任何形式转载请联系作者.作者:U_U(来自豆瓣)来源:https://www.douban.com/note/289846168/ 调试前端页面我一直使用着神器Chrome开发人员工具 ...

  3. .net core C# DataTable 和List之间相互转换的方法

    一.List<T>/IEnumerable转换到DataTable/DataView 方法一: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1 ...

  4. C# 关于图片转ICO的代码整理(无损,不需要第三方类库)

    概述(Overview) 感觉网上文章整理的不全,我这边做个专栏,专门做这个事情吧,节省大家搜索.筛选.整理的时间精力.有用可以点个赞.引用本文章请注明出处,谢谢. (I feel that the ...

  5. JavaSE的运算符

    [Top] 算术运算 分类 运算符 算数运算符 * / + - % ++ -- 赋值运算符 +=, -=, /=, *=, %=(扩展赋值运算符), = 比较(关系)运算符 == != > &l ...

  6. HDFS 常用操作命令

    HDFS 文件操作命令 注,其实常用命令不用网上搜,和linux下的命令很类似,触类旁通,直接在linux 上 hadoop fs 看一下就行了,不需要刻意去记 我把 linux 上的 help 列举 ...

  7. 大一统的监控探针采集器 cprobe

    需求背景 监控数据采集领域,比如 Prometheus 生态有非常多的 Exporter,虽然生态繁荣,但是无法达到开箱即用的大一统体验,Exporter 体系的核心问题有: 良莠不齐:有的 Expo ...

  8. 暴走漫画系列之高仿淘宝收货地址(附demo)

    引语: 我是个程序猿,一天我坐在路边一边喝水一边苦苦检查bug. 这时一个乞丐在我边上坐下了,开始要饭,我觉得可怜,就给了他1块钱. 然后接着调试程序.他可能生意不好,就无聊的看看我在干什么,然后过了 ...

  9. jenkins发布服务失败查看catalina.out启动日志和xxl-job jobhandler naming conflicts

    jenkins发布服务失败查看catalina.out启动日志和xxl-job jobhandler naming conflicts 1.查看tomcat/logs/catalina.out 日志, ...

  10. Kubernetes(K8s)之Pod

    Pod介绍 Pod是K8s的最小调度单位 内部是一组Container容器,根容器Pause和其他业务容器 拥有唯一Pod IP 小贴士: 在生产环境中,极少单独Pod的情况 一般都是使用Deploy ...