虚拟线程是在 Java 21 版本中实现的一种轻量级线程。它由 JVM 进行创建以及管理。虚拟线程和传统线程(我们称之为平台线程)之间的主要区别在于,我们可以轻松地在一个 Java 程序中运行大量、甚至数百万个虚拟线程。

由于虚拟线程的数量众多,也就赋予了 Java 程序强大的力量。虚拟线程适合用来处理大量请求,它们可以更有效地运行 “一个请求一个线程” 模型编写的 web 应用程序,可以提高吞吐量以及减少硬件浪费。

由于虚拟线程是 java.lang.Thread 的实现,并且遵守自 Java SE 1.0 以来指定 java.lang.Thread 的相同规则,因此开发人员无需学习新概念即可使用它们。

但是虚拟线程才刚出来,对我们来说有一些陌生。由于 Java 历来版本中无法生成大量平台线程(多年来 Java 中唯一可用的线程实现),已经让程序员养成了一套关于平台线程的使用习惯。这些习惯做法在应用于虚拟线程时会适得其反,我们需要摒弃。

此外虚拟线程和平台线程在创建成本上的巨大差异,也提供了一种新的关于线程使用的方式。Java 的设计者鼓励使用虚拟线程而不必担心虚拟线程的创建成本。

本文无意全面涵盖虚拟线程的每个重要细节,目的是给大家使用虚拟线程提供一套使用指南,帮助大家能更好使用的虚拟线程,发挥其作用并避免踩坑。

本文完整大纲如下,

使用信号量限制并发

在某些场景下,我们需要限制某个操作的并发数。例如某些外部服务可能无法同时处理超过 10 个并发请求。

由于平台线程是一种宝贵的资源,通常在线程池中进行管理,因此线程池的使用对于如今的程序员相当普遍。

比如上面例子要限制并发请求数,某些人会使用线程池来处理,代码如下,

ExecutorService es = Executors.newFixedThreadPool(10);
...
Result foo() {
try {
var fut = es.submit(() -> callLimitedService());
return f.get();
} catch (...) { ... }
}

上面代码示例可以确保外部服务最多只有 10 个并发请求,因为我们的线程池中只有最多 10 个线程。

限制并发只是使用线程池的副产品。线程池旨在共享稀缺资源,而虚拟线程并不稀缺,因此永远不应该池化虚拟线程!

使用虚拟线程时,如果要限制访问某些服务的并发请求,则应该使用专门为此目的设计的 Semaphore 类。示例代码如下,

Semaphore sem = new Semaphore(10);
...
Result foo() {
sem.acquire();
try {
return callLimitedService();
} finally {
sem.release();
}
}

在这个示例中,同一时刻只有 10 个虚拟线程可以进入 foo() 方法取得锁,而其他虚拟线程将会被阻塞。

简单地使用信号量阻塞某些虚拟线程可能看起来与将任务提交到固定数量线程池有很大不同,但事实并非如此。

将任务提交到等待任务池会将它们排队处理,信号量在内部(或任何其他阻塞同步构造)构造了一个阻塞线程队列,这些任务在阻塞线程队列上也会进行排队处理。

我们可以将平台线程池认作是从等待任务队列中提取任务进行处理的工作人员,然后将虚拟线程视为任务本身,在任务或者线程可以执行之前将会被阻塞,但它们在计算机中的底层表示上实际是相同的。

这里想告诉大家的就是不管是线程池的任务排队,还是信号量内部的线程阻塞,它们之间是由等效性的。在虚拟线程某些需要限制并发数场景下,直接使用信号量即可。

不要在线程局部变量中缓存可重用对象

虚拟线程支持线程局部变量,就像平台线程一样。通常线程局部变量用于将一些特定于上下文的信息与当前运行的代码关联起来,例如当前事务和用户 ID。

对于虚拟线程来说,使用线程局部变量是完全合理的。但是如果考虑更安全、更有效的线程局部变量,可以使用 Scoped Values。

更多有关 Scoped Values 介绍,请参阅 https://docs.oracle.com/en/java/javase/21/core/scoped-values.html#GUID-9A4565C5-82AE-4F03-A476-3EAA9CDEB0F6

线程局部变量有一种用途与虚拟线程是不太适合的,那就是缓存可重用对象。

可重用对象的创建成本通常很高,通常消耗大量内存且可变,还不是线程安全的。它们被缓存在线程局部变量中,以减少它们实例化的次数以及它们在内存中的实例数量,好处是它们可以被线程上不同时间运行的多个任务重用,避免昂贵对象的重复创建。

例如 SimpleDateFormat 的实例创建成本很高,而且不是线程安全的。为了解决创建成本、线程不安全问题,通常是将此类实例缓存在 ThreadLocal 中,如下例所示:

static final ThreadLocal<SimpleDateFormat> cachedFormatter =
ThreadLocal.withInitial(SimpleDateFormat::new); void foo() {
...
cachedFormatter.get().format(...);
...
}

仅当线程(以及因此在线程本地缓存的昂贵对象)被多个任务共享和重用时(就像平台线程被池化时的情况一样),这种缓存才有用。许多任务在线程池中运行时可能会调用 foo,但由于池中仅包含几个线程,因此该对象只会被实例化几次(每个池线程一次)并被缓存和重用。

但是虚拟线程永远不会被池化,也不会被不相关的任务重用。因为每个任务都有自己的虚拟线程,所以每次从不同任务调用 foo 都会触发新 SimpleDateFormat 的实例化。而且由于可能有大量的虚拟线程同时运行,昂贵的对象可能会消耗相当多的内存。这些结果与线程本地缓存想要实现的结果恰恰相反。

对于线程局部变量缓存可重用对象的问题,没有什么好的通用替代方案,但对于 SimpleDateFormat,我们应该将其替换为 DateTimeFormatter。DateTimeFormatter 是不可变的,因此单个实例就可以由所有线程共享,

static final DateTimeFormatter formatter = DateTimeFormatter….;

void foo() {
...
formatter.format(...);
...
}

需要注意的是,使用线程局部变量来缓存共享的昂贵对象有时是由一些异步框架在幕后完成的,其隐含的假设是这些可重用对象只会由极少数池线程使用。

所以混合虚拟线程和异步框架一起使用可能不是一个好主意,对某些方法的调用可能会导致可重用对象被重复创建。

避免长时间和频繁的 synchronized

当前虚拟线程实现由一个限制是,在同步块或方法内执行 synchronized 阻塞操作会导致 JDK 的虚拟线程调度程序阻塞宝贵的操作系统线程,而如果阻塞操作是在同步块或方法外完成的,则不会被阻塞。我们称这种情况为 “Pinning”。

如果阻塞操作既长期又频繁,则 “Pinning” 可能会对服务器的吞吐量产生不利影响。如果阻塞操作短暂(例如内存中操作)或不频繁则可能不会产生不利影响。

为了检测可能有害的 “Pinning” 实例,(JDK Flight Recorder (JFR) 在 “Pinning” 阻塞时间超过 20 毫秒时,会发出 jdk.VirtualThreadPinned 事件。

或者我们可以使用系统属性 jdk.tracePinnedThreads 在线程被 “Pinning” 阻塞时发出堆栈跟踪。

启动 Java 程序时添加 -Djdk.tracePinnedThreads=full 运行,会在线程被 “Pinning” 阻塞时打印完整的堆栈跟踪,突出显示本机帧和持有监视器的帧。使用 -Djdk.tracePinnedThreads=short 运行,会将输出限制为仅有问题的帧。

如果这些机制检测到既长期又频繁 “Pinning” 的地方,请在这些特定地方将 synchronized 替换为 ReentrantLock。以下是长期且频繁使用 synchronized 的示例,

synchronized(lockObj) {
frequentIO();
}

我们可以将其替换为以下内容:

lock.lock();
try {
frequentIO();
} finally {
lock.unlock();
}

参考资料

最后说两句

针对虚拟线程的使用,相信大家心里已经有了答案。在对虚拟线程需要限制并发数的场景,使用信号量即可。在虚拟线程中使用线程局部变量时要注意避免缓存昂贵的可重用对象。对于使用到 synchronized 同步块或者方法的虚拟线程,建议替换为 ReentrantLock,避免影响吞吐量。

关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!

Java 新技术:虚拟线程使用指南(二)的更多相关文章

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

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

  2. java中的线程问题(二)——线程的创建和用法。

    在java中一个类要当作线程来使用有两种方法. 1.继承Thread类,并重写run函数 2.实现Runnable接口,并重写run函数 因为java是单继承的,在某些情况下一个类可能已经继承了某个父 ...

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

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

  4. Java 线程池(二)

    简介 在上篇 Java 线程池(一) 我们介绍了线程池中一些的重要参数和具体含义,这篇我们看一看在 Java 中是如何去实现线程池的,要想用好线程池,只知其然是远远不够的,我们需要深入实现源码去了解线 ...

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

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

  6. Java SE 19 虚拟线程

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

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

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

  8. Java线程volatile(二)

    volatile:使变量在多个线程中可见 在java 中每个线程都会有一块工作内存区,其中存放着所有线程共享的主内存中变量的拷贝.当线程执行时,在自己的工作内存区操作这些变量,为了存取一个共享的变量, ...

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

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

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

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

随机推荐

  1. 基于go语言gin框架的web项目骨架

    该骨架每个组件之间可单独使用,组件之间松耦合,高内聚,组件的实现基于其他三方依赖包的封装. 目前该骨架实现了大多数的组件,比如事件,中间件,日志,配置,参数验证,命令行,定时任务等功能,目前可以满足大 ...

  2. 由mysql转为oracle注意事项

    1. text类型->nclob 2. 表名是关键字   引号 3. 字段名是关键字 引号 4. 日期型和mysql不同,hh24:mi:ss  就是HH:mm:ss,库里存日期型也可以(查询时 ...

  3. P1522 [USACO2.4] 牛的旅行 Cow Tours

    Problem 题目简述 给你两个独立的联通块,求:在两个联通块上各找一个点连起来,使得新的联通块的直径的最小值. 思路 本题主要做法:\(Floyd\). 首先,Floyd求出任意两个点之间的最短路 ...

  4. Azure Data Factory(十)Data Flow 组件详解

    一,引言 随着大数据技术的不断发展,数据处理和分析变得越来越重要.为了满足企业对数据处理的需求,微软推出了 Azure Data Factory (ADF),它是一个云端的数据集成服务,用于创建.安排 ...

  5. C语言【进制、原反补码、数据类型】

    C语言[进制.原反补码.数据类型] 1.二进制.八进制.十进制.十六进制在C语言中的使用(赋值及输出)? /** 二进制 赋值时以 0b 或 0B 开头: 输出时没有所属占位符, 十六进制整数输出也比 ...

  6. C语言【编译器、变量、输入输出有关的】

    C语言[编译器.变量.输入输出有关的] 一些想到的[从编译器到变量到输入输出有关的]的问题,有些是按自己理解写的答,有些待解决. C语言可以跨平台,汇编不可以,编译时C语言会根据不同系统翻译成不同形式 ...

  7. Codeforces Round #707 (Div. 2)A~C题解

    写在前边 链接:Codeforces Round #707 (Div. 2) 心态真的越来越不好了,看A没看懂,赛后模拟了一遍就过了,B很简单,但是漏了个判断重复的条件. A. Alexey and ...

  8. 哈希表(hash)

    散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构.也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度 ...

  9. Numpy理解

    目录 什么是numpy numpy的安装 numpy数组 定义numpy数组 numpy数组的相关功能 基本操作 0数组和1数组 随机数组 二维数组 numpy的数组操作 我们再平常学习python和 ...

  10. 一套开源、强大且美观的WPF UI控件库 - HandyControl

    前言 今天给大家推荐一套开源.强大且美观的WPF UI控件库:HandyControl. WPF介绍 WPF 是一个强大的桌面应用程序框架,用于构建具有丰富用户界面的 Windows 应用.它提供了灵 ...