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); //or Thread.startVirtualThread(() -> {
//Code to execute in virtual thread
System.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.run
private static final int STARTED = 1;
// 可执行,尚未分配平台线程
private static final int RUNNABLE = 2;
// 可执行,已分配平台线程
private static final int RUNNING = 3;
// 线程尝试park
private static final int PARKING = 4;
// 从平台线程卸载
private static final int PARKED = 5;
// cont.yield失败,未从平台线程卸载
private static final int PINNED = 6;
// 尝试cont.yield
private 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 历史上的一个里程碑特性。使用虚拟线程,一个程序可以用少量的物理内存和计算资源处理数百万个线程,这是传统平台线程无法做到的。当与结构化并发相结合时,它还将导致编写更好的程序。

作者:京东科技 宋慧超

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

聊聊JDK19特性之虚拟线程的更多相关文章

  1. 虚拟线程 - VirtualThread源码透视

    前提 JDK19于2022-09-20发布GA版本,该版本提供了虚拟线程的预览功能.下载JDK19之后翻看了一下有关虚拟线程的一些源码,跟早些时候的Loom项目构建版本基本并没有很大出入,也跟第三方J ...

  2. 支持JDK19虚拟线程的web框架,之一:体验

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于虚拟线程 随着JDK19 GA版本的发布,虚拟线程 ...

  3. 支持JDK19虚拟线程的web框架,之二:完整开发一个支持虚拟线程的quarkus应用

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本篇是<支持JDK19虚拟线程的web ...

  4. 支持JDK19虚拟线程的web框架,之五(终篇):兴风作浪的ThreadLocal

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos <支持JDK19虚拟线程的web框架>系列 ...

  5. Java的虚拟线程(协程)特性开启预览阶段,多线程开发的难度将大大降低

    高并发.多线程一直是Java编程中的难点,也是面试题中的要点.Java开发者也一直在尝试使用多线程来解决应用服务器的并发问题.但是多线程并不容易,为此一个新的技术出现了,这就是虚拟线程. 传统多线程的 ...

  6. 在Tomcat中启用虚拟线程特性

    前提 趁着国庆前后阅读了虚拟线程相关的源码,写了一篇<虚拟线程 - VirtualThread源码透视>,里面介绍了虚拟线程的实现原理和使用示例.需要准备做一下前期准备: 安装OpenJD ...

  7. 支持JDK19虚拟线程的web框架之四:看源码,了解quarkus如何支持虚拟线程

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 前文链接 支持JDK19虚拟线程的web框架,之一:体 ...

  8. Java19虚拟线程都来了,我正在写的线程代码会被淘汰掉吗?

    Java19中引入了虚拟线程,虽然默认是关闭的,但是可以以Preview模式启用,这绝对是一个重大的更新,今天Java架构杂谈带大家开箱验货,看看这家伙实现了什么了不起的功能. 1 为什么需要虚拟线程 ...

  9. Java将增加虚拟线程,挑战Go协程

    我们知道 Go 语言最大亮点之一就是原生支持并发,这得益于 Go 语言的协程机制.一个 go 语句就可以发起一个协程 (goroutin).协程本质上是一种用户态线程,它不需要操作系统来进行调度,而是 ...

  10. Java SE 19 虚拟线程

    Java SE 19 虚拟线程 作者:Grey 原文地址: 博客园:Java SE 19 虚拟线程 CSDN:Java SE 19 虚拟线程 说明 虚拟线程(Virtual Threads)是在Pro ...

随机推荐

  1. 01-C语言基础语法

    目录 一. C语言发展史 二. 数据类型 三. 常量和变量 四. 字符串和转义字符 五. 选择语句 六. 循环语句 七. 函数 一. C语言发展史 1963 年ALGOL 60 作为C语言最早的模型, ...

  2. C# - DTO 的字符串表达

    第一阶段 重写 DTO 的 ToString() 方法.利用 Newtonsoft.Json 序列化 DTO 对象. 第二阶段 为 DTO 设置基类,重写基类的 ToString() 方法.利用 Sy ...

  3. golang 实现四层负载均衡

    大家好,我是蓝胖子,做开发的同学应该经常听到过负载均衡的概念,今天我们就来实现一个乞丐版的四层负载均衡,并用它对mysql进行负载均衡测试,通过本篇你可以了解到零拷贝的应用,四层负载均衡的本质以及实践 ...

  4. Python潮流周刊#9:如何在本地部署开源大语言模型?

    你好,我是猫哥.这里每周分享优质的 Python 及通用技术内容,部分为英文,已在小标题注明.(标题取自其中一则分享,不代表全部内容都是该主题,特此声明.) 首发于我的博客:https://pytho ...

  5. Kali下载安装以及基础配置

    Kali官网:Kali Linux | Penetration Testing and Ethical Hacking Linux Distribution Kali下载地址:Get Kali | K ...

  6. CentOS 7 搭建NFS服务器

    服务端安装 # 创建挂载目录 cd ~ cd data/ mkdir www-content cd www-content/ pwd # 安装软件 yum install nfs-utils yum ...

  7. MapReduce实现TopN的效果

    1.背景 最近在学习Hadoop的MapReduce,此处记录一下如何实现 TopN 的效果,以及在MapReduce中如何实现 自定义分组. 2.需求 我们有一份数据,数据中存在如下3个字段,订单编 ...

  8. 一张表实现类某音的视频"评论回复"功能

    ​前言 现如今,不管是哪种类型的应用,评论区都少不了.从工具类的到媒体信息流类的,评论留言都是最基本的互动环节.比如抖音短视频下,针对视频每个用户都可以发表自己的观点:而针对用户的评论,其他的用户又可 ...

  9. git 访问仓库错误

    通过https访问git出现错误, failed: Error in the pull function 尝试将https改为http

  10. 【技术积累】Vue中的核心概念【四】

    Vue的生命周期 Vue中的生命周期是指组件从创建到销毁的整个过程中,会触发一系列的钩子函数 Vue2中的生命周期 Vue2中的生命周期钩子函数是在组件的不同阶段执行的特定函数.这些钩子函数允许开发者 ...