聊聊JDK19特性之虚拟线程 | 京东云技术团队

1.前言

在读《深入理解JVM虚拟机》这本书前两章的时候整理了JDK从1.0到最新版本发展史,其中记录了JDK这么多年来演进过程中的一些趣闻及引人注目的一些特性,在调研JDK19新增特性的时候了解到了虚拟线程这个概念,于是对虚拟线程进行学习整理内容如下。

2.虚拟线程介绍?

虚拟线程(Virtual Threads)就犹如名字一样,并非传统意义上的JAVA线程。传统意义上的JAVA线程(以下称为平台线程)跟操作系统的内核线程是一一映射的关系(如图1所示)。而对于平台线程的创建和销毁所带来的开销是非常大的,所以JAVA采用线程池的方式来维护平台线程而避免线程的反复创建和销毁。

然而平台线程也会占用内存、CPU资源,往往在CPU和网络连接成为系统瓶颈前,平台线程首当其冲的会成为系统瓶颈。在单台服务器硬件资源确定的情况下,平台线程的数量同样也会因为硬件资源而受到限制,也成为单台服务器吞吐量提升的主要障碍。

图1 平台线程和系统线程映射关系

谈回虚拟线程,虚拟线程则是由JDK而非操作系统提供的一种线程轻量级实现,它相较于平台线程而言具有以下特性:

  • 不依赖于平台线程的数量;
  • 不会增加额外的上下文切换开销;
  • 不会在代码的整个生命周期中阻塞系统线程;
  • 整个虚拟线程的维护是通过JVM进行管理,作为普通的JAVA对象存放在RAM中。

那么意味着若干的虚拟线程可以在同一个系统线程上运行应用程序的代码(如图2所示),只有在虚拟线程执行的时候才会消耗系统线程,在等待和休眠时不会阻塞系统线程。

图2 虚拟线程和平台线程映射关系

相较于平台线程而言,虚拟线程是一种非常廉价和丰富的线程,可以说虚拟线程的数量是一种近乎于无限多的线程,它对硬件的利用率接近于最好,在相同硬件配置服务器的情况下,虚拟线程比使用平台线程具备更高的并发性,从而提升整个应用程序的吞吐量。如果说平台线程和系统线程调度为1:1的方式,虚拟线程则采用M:N的调度方式,其中大量的虚拟线程M在较少的系统线程N上运行。

3.虚拟线程如何被JVM调度呢?

图3 JVM调度虚拟线程流程图

  • 先创建一个虚拟线程,此时JVM会将虚拟线程装载在平台线程上,平台线程则会去绑定一个系统线程。
  • JVM会使用调度程序去使用调度线程执行虚拟线程中的任务。
  • 任务执行完成之后清空上下文变量,将调度线程返还至调度程序等待处理下一个任务。

4.虚拟线程的目标、非目标?

目标:

  • 为java.lang.Thread增加一种额外的实现,即虚拟线程,它能做到在几个G的JVM堆上创建几百万个活动的虚拟线程(这在现在的JDK中几乎不可能实现),并且表现出和现在的线程几乎一样的行为。
  • 对虚拟线程问题定位也可以通过已经存在的JDK工具,尽可能保持和现在的线程相似的方式。

在 Java 中,经典线程是 java.lang.Thread 类的实例。后面我们也将它们称为平台线程。

非目标:

  • 虚拟线程不是为了改变现在这种操作系统级别的线程的实现。
  • 虚拟线程不是为了自动将已经存在的线程构造方法自动转为虚拟线程。
  • 虚拟线程不是为了改变JMM。
  • 虚拟线程不是为了增加一种新的内部线程通信机制。
  • 除了并行流之外,虚拟线程也不是为了提供一种新的数据并行结构。

5.如何创建虚拟线程?

  • 使用Thread.startVirtualThread()

此方法创建一个新的虚拟线程来执行给定的 Runnable 任务。

Runnable runnable = () -> System.out.println("Virtual Thread");
Thread.startVirtualThread(runnable);//orThread.startVirtualThread(() -> {//Code to execute in virtual threadSystem.out.println("Virtual Thread");
});
  • 使用Thread.Builder

如果我们想在创建线程后显式启动它,我们可以使用 Thread.ofVirtual() 返回一个 VirtualThreadBuilder 实例。它的 start() 方法启动一个虚拟线程。这里的 Thread.ofVirtual().start(runnable) 等价于 Thread.startVirtualThread(runnable)。

ThreadFactory factory = Thread.ofVirtual().factory();

我们可以使用Thread.Builder引用来创建和启动多个线程。

Runnable runnable = () -> System.out.println("Virtual Thread");Thread.Builder builder = Thread.ofVirtual().name("Virtual-Thread");Thread t1 = builder.start(runnable); 
Thread t2 = builder.start(runnable);

类似的 APIThread.ofPlatform()也可用于创建平台线程。

Thread.Builder builder = Thread.ofPlatform().name("Platform-Thread");Thread t1 = builder.start(() -> {...}); 
Thread t2 = builder.start(() -> {...});
  • 使用Executors.newVirtualThreadPerTaskExecutor()

此方法为每个任务创建一个新的虚拟线程。 Executor 创建的线程数是无限的。

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

6.平台线程和虚拟线程的区别?

图4 虚拟线程生命周期

  • 虚拟线程始终是守护线程。 Thread.setDaemon(false) 方法不能将虚拟线程更改为非守护线程。

请注意,当所有启动的非守护线程都终止时,JVM 终止。这意味着 JVM 在退出之前不会等待虚拟线程完成。

Thread virtualThread = ...; //创建虚拟线程
//virtualThread.setDaemon(true);  //没有作用
  • 虚拟线程始终具有正常优先级,并且即使使用setPriority(n)方法,也无法更改优先级。在虚拟线程上使用此方法无效。
Thread virtualThread = ...; //创建虚拟线程
//virtualThread.setPriority(Thread.MAX_PRIORITY);  //没有作用
  • 虚拟线程不是线程组的活动成员。在虚拟线程上调用时,Thread.getThreadGroup()返回一个名为VirtualThreads的占位符线程组。
  • 虚拟线程不支持stop()、suspend()或resume()方法。

这些方法在虚拟线程上调用时会引发UnsupportedOperationException。

  • 虚拟线程由JVM调度,JVM将VT分配给平台线程的动作称为挂载(mount),取消分配的动作称为卸载(unmount),线程状态如下
    // 初始状态private static final int NEW      = 0;// 线程启动,由于虚拟线程的run()是个空方法,此时尚未开始执行任务// 真正的任务执行在cont.runprivate static final int STARTED  = 1;// 可执行,尚未分配平台线程private static final int RUNNABLE = 2;// 可执行,已分配平台线程private static final int RUNNING  = 3;// 线程尝试parkprivate static final int PARKING  = 4;// 从平台线程卸载private static final int PARKED   = 5;// cont.yield失败,未从平台线程卸载private static final int PINNED   = 6;// 尝试cont.yieldprivate static final int YIELDING = 7;// 终结态private static final int TERMINATED = 99;

7.实例场景分析平台线程和虚拟线程的性能:

任务说明:在控制台中打印一条消息之前等待1秒,现在使用Runnable创建10000个线程,用虚拟线程和平台线程执行它们,来比较两者的性能。我们将使用Duration.between()api 来测量执行所有任务的经过时间。

首先,我们使用一个包含 100 个平台线程的池。这样,Executor 一次可以运行 100 个任务,其他任务需要等待。由于我们有 10,000 个任务,因此完成执行的总时间约为 100 秒。

Instant start = Instant.now();try (var executor = Executors.newFixedThreadPool(100)) {for(int i = 0; i < 10_000; i++) {executor.submit(runnable);}
}Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();  
System.out.println("Total elapsed time : " + timeElapsed);	

输出

Total elapsed time : 101152 //大概 101 秒

接下来,我们将Executors.newFixedThreadPool(100)替换为Executors.newVirtualThreadPerTaskExecutor()。这将在虚拟线程而不是平台线程中执行所有任务。

Instant start = Instant.now();try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {for(int i = 0; i < 10_000; i++) {executor.submit(runnable);}
}Instant finish = Instant.now();
long timeElapsed = Duration.between(start, finish).toMillis();  
System.out.println("Total elapsed time : " + timeElapsed);	

输出

Total elapsed time : 1589 // 大概 1.5 秒

请注意虚拟线程的超快性能将执行时间从 100 秒减少到 1.5 秒,而 Runnable 代码没有任何变化。 其他实际场景:https://zhuanlan.zhihu.com/p/604507117?utm_id=0

8.使用虚拟线程需要注意什么?

  • 不要建虚拟线程池

Java 线程池旨在避免创建新操作系统线程的开销,因为创建它们是一项昂贵的操作。但是创建虚拟线程并不昂贵,因此永远不需要将它们池化。建议每次需要时创建一个新的虚拟线程。 请注意,使用虚拟线程后,我们的应用程序可能能够处理数百万个线程,但其他系统或平台一次只能处理几个请求。例如,我们可以只有几个数据库连接或与其他服务器的网络连接。 在这些情况下,也不要使用线程池。相反,使用信号量来确保只有指定数量的线程正在访问该资源。

private static final Semaphore SEMAPHORE = new Semaphore(50);SEMAPHORE.acquire();try {// 信号量被控制在 50 来访问请求// 访问数据库或资源
} finally {SEMAPHORE.release();
}
  • 避免使用线程局部变量 (ThreadLocal)

虚拟线程支持线程局部行为的方式与平台线程相同,但由于虚拟线程可以创建数百万个,因此只有在仔细考虑后才能使用线程局部变量。 例如,如果我们在应用程序中扩展一百万个虚拟线程,那么将有一百万个 ThreadLocal 实例以及它们所引用的数据。如此大量的实例会给内存带来很大的负担,应该避免。

  • 使用 ReentrantLock 而不是同步块

有两种特定场景,虚拟线程可以阻塞平台线程(称为 OS 线程的固定)。 1、当它在同步块或同步方法内执行代码时 2、当它执行本地方法或外部函数时 这种同步块不会使应用程序出错,但它会限制应用程序的可扩展性,类似于平台线程。 如果一个方法使用非常频繁并且它使用同步块,则考虑将其替换为 ReentrantLock 机制。

public synchronized void m() {try {// ... 访问资源} finally {//}
}
private final ReentrantLock lock = new ReentrantLock();public void m() {lock.lock();  // 阻塞try {// ... 访问资源} finally {lock.unlock();}
}

9.结论

长期以来,传统的 Java 线程一直很好用。随着微服务领域对可扩展性和高吞吐量的需求不断增长,虚拟线程将被证明是 Java 历史上的一个里程碑特性。使用虚拟线程,一个程序可以用少量的物理内存和计算资源处理数百万个线程,这是传统平台线程无法做到的。当与结构化并发相结合时,它还将导致编写更好的程序。

作者:京东科技 宋慧超

来源:京东云开发者社区 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/152688.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【单片机】17-温度传感器DS18B20

1.DS18B20相关背景知识 1.温度传感器 &#xff08;1&#xff09;测温度的方式&#xff1a;物理&#xff08;汞柱&#xff0c;气压&#xff09;&#xff0c;电子&#xff08;金属电性能随温度变化&#xff09; &#xff08;2&#xff09;早期&#xff1a;热敏电阻&#xff08;模…

零基础快速自学SQL,2天足矣。

此文是《10周入门数据分析》系列的第6篇。 想了解学习路线&#xff0c;可以先行阅读“ 学习计划 | 10周入门数据分析 ” 上一篇分享了数据库的基础知识&#xff0c;以及如何安装数据库&#xff0c;今天这篇分享数据库操作和SQL。 SQL全称是 Structured Query Language&#x…

聊聊僵尸进程

文章目录 1. 前言1.1 什么是僵尸进程1.2 为什么需要关注僵尸进程 2. 僵尸进程的产生2.2 为什么会产生僵尸进程2.3 举个栗子 3. 僵尸进程的影响3.1 僵尸进程为何会占用系统资源3.2 操作系统如何知道哪个资源需要被释放3.3 什么是进程表3.4 什么是PCB 5. 如何处理僵尸进程4.1 识别…

ViewPager、RecycleView实现轮播图

1.ViewPager实现轮播图形效果。 1&#xff09;layout中&#xff0c;PageIndicatorView轮播的View <RelativeLayoutandroid:layout_width"match_parent"android:layout_height"200dp"android:orientation"vertical"><androidx.viewpager…

【19】c++设计模式——>桥接模式

桥接模式的定义 C的桥接模式&#xff08;Bridge Pattern&#xff09;是一种结构型设计模式&#xff0c;它将抽象部分与实现部分分离&#xff0c;使得它们可以独立地变化。桥接模式的核心思想是利用组合关系代替继承关系&#xff0c;将系统划分成多个独立的、功能不同的类层次结…

TypeScript 笔记:String 字符串

1 对象属性 length 返回字符串的长度 2 对象方法 charAt() 返回在指定位置的字符 charCodeAt() 返回在指定的位置的字符的 Unicode 编码 concat 连接两个或更多的字符串 indexOf 返回某个指定的字符串值在字符串中首次出现的位置 lastIndexOf 从后向前搜索字符串&…

ctfshow-web5(md5弱比较)

打开题目链接是html和php代码 html没啥有用信息&#xff0c;这里审一下php代码 &#xff1a; 要求使用get方式传入两个参数 v1&#xff0c;v2 ctype_alpha()函数&#xff1a;用于检查给定的字符串是否仅包含字母&#xff1b; is_numeric()函数&#xff1a;检测字符串是否只由…

VC6 MFC Dialog as apllication 编程

MFC框架虽然古老&#xff0c;但编程还是方便多了&#xff0c;在操控界面控件的基础上&#xff0c;平时在Console模式习练的类可以融入到MFC中&#xff0c;开发应用程序还是比较方便的。下图是习练程序的界面。 说明&#xff1a; 一个EDIT框&#xff0c;一个Label框&#xff1b…

【python海洋专题十四】读取多个盐度nc数据画盐度季节变化图

本期内容 读取多个盐度文件&#xff1b;拼接数据在画盐度的季节分布图Part01. 使用数据 IAP 网格盐度数据集 数据详细介绍&#xff1a; 见文件附件&#xff1a; pages/file/dl?fid378649712527544320 全球温盐格点数据.pdf IAP_Global_ocean_gridded_product.pdf 全球温…

第三课-软件升级-Stable Diffusion教程

前言: 虽然第二课已经安装好了 SD,但你可能在其它地方课程中,会发现很多人用的和你的界面差距很大。这篇文章会讲一些容易忽略或者常常需要做的操作,不一定要完全照做,以后再回过头看看也可以。 1.控制类型 问题:为什么别人有“控制类型”部分,而我没有?如下红色方框…

linux centos Python + Selenium+Chrome自动化测试环境搭建?

在 CentOS 系统上搭建 Python Selenium Chrome 自动化测试环境&#xff0c;需要执行以下步骤&#xff1a; 1、安装 Python CentOS 7 自带的 Python 版本较老&#xff0c;建议使用 EPEL 库或源码安装 Python 3。例如&#xff0c;使用 EPEL 库安装 Python 3&#xff1a; sud…

Pytorch笔记之回归

文章目录 前言一、导入库二、数据处理三、构建模型四、迭代训练五、结果预测总结 前言 以线性回归为例&#xff0c;记录Pytorch的基本使用方法。 一、导入库 import numpy as np import matplotlib.pyplot as plt import torch from torch.autograd import Variable # 定义求…

「专题速递」AR协作、智能NPC、数字人的应用与未来

元宇宙是一个融合了虚拟现实、增强现实、人工智能和云计算等技术的综合概念。它旨在创造一个高度沉浸式的虚拟环境&#xff0c;允许用户在其中交互、创造和共享内容。在元宇宙中&#xff0c;人们可以建立虚拟身份、参与虚拟社交&#xff0c;并享受无限的虚拟体验。 作为互联网大…

Prompt-Tuning(一)

一、预训练语言模型的发展过程 第一阶段的模型主要是基于自监督学习的训练目标&#xff0c;其中常见的目标包括掩码语言模型&#xff08;MLM&#xff09;和下一句预测&#xff08;NSP&#xff09;。这些模型采用了Transformer架构&#xff0c;并遵循了Pre-training和Fine-tuni…

上班第一天同事让我下载个小乌龟,我就去百度小乌龟。。。。

记得那会儿是刚毕业&#xff0c;去上班第一天&#xff0c;管我的那个上级说让我下载个小乌龟&#xff0c;等下把代码拉一下&#xff0c;我那是一脸懵逼啊&#xff0c;我在学校只学过git啊&#xff0c;然后开始磨磨蹭蹭吭吭哧哧的不知所措&#xff0c;之后我想也许百度能救我&am…

C语言内存函数

目录 memcpy(Copy block of memory)使用和模拟实现memcpy的模拟实现 memmove(Move block of memory)使用和模拟实现memmove的模拟实现: memset(Fill block of memory)函数的使用扩展 memcmp(Compare two blocks of memory)函数的使用 感谢各位大佬对我的支持,如果我的文章对你有…

PSN 两步验证解除2023.10.9经验贴

背景 本人10月1号收到Sony邮件&#xff0c;说是不规律登录&#xff0c;需修改密码后登录&#xff0c;然后我10月8日登录PS4的时候&#xff0c;提示两步验证。当时就想坏了&#xff0c;然后找B站相关经验贴&#xff0c;10月9号电话香港客服&#xff0c;解除了两步验证&#xff0…

Windows10打开应用总是会弹出提示窗口的解决方法

用户们在Windows10电脑中打开应用程序&#xff0c;遇到了总是会弹出提示窗口的烦人问题。这样的情况会干扰到用户的正常操作&#xff0c;给用户带来不好的操作体验&#xff0c;接下来小编给大家详细介绍关闭这个提示窗口的方法&#xff0c;让大家可以在Windows10电脑中舒心操作…

计算机网络八股

1、请你说说TCP和UDP的区别 TCP提供面向连接的可靠传输&#xff0c;UDP提供面向无连接的不可靠传输。UDP在很多实时性要求高的场景有很好的表现&#xff0c;而TCP在要求数据准确、对速度没有硬件要求的场景有很好的表现。TCP和UDP都是传输层协议&#xff0c;都是为应用层程序服…

大数据——Spark Streaming

是什么 Spark Streaming是一个可扩展、高吞吐、具有容错性的流式计算框架。 之前我们接触的spark-core和spark-sql都是离线批处理任务&#xff0c;每天定时处理数据&#xff0c;对于数据的实时性要求不高&#xff0c;一般都是T1的。但在企业任务中存在很多的实时性的任务需求&…