前言

在 Java 中,虚拟线程 (JEP-425) 是 JVM 管理的轻量级线程,它有助于我们编写高吞吐量并发应用程序。

1. Java线程模型和虚线程

1.1 平台线程

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

传统上,Java 将平台线程视为围绕操作系统 (OS) 线程的瘦包装器。创建这样的平台线程一直很昂贵(由于操作系统维护的堆栈和其他资源很大),因此 Java 一直使用线程池来避免线程创建的开销。

平台线程的数量也必须受到限制,因为这些非常消耗资源的线程会影响整个机器的性能,这主要是因为平台线程被 1:1 映射到 OS 线程。通常,在CPU,网络连接等成为系统瓶颈前,相当数量的平台线程会首先成为系统的瓶颈。

换句话说,在硬件资源稍有冗余的前提下,平台线程首先成为了系统吞吐量的瓶颈。

1.2 平台线程的可扩展性问题

平台线程一直很容易建模、编程和调试,因为它们使用平台的并发单元来表示应用程序的并发单元。它被称为一个线程一个请求的模式。

但是这种模式限制了服务器的吞吐量,因为并发请求的数量(服务器可以处理)与服务器的硬件性能成正比。因此,即使在多核处理器中,可用线程的数量也必须受到限制。

除了线程数量之外,延迟也是一个大问题。如果我们仔细观察,在当今的微服务世界中,请求是通过在多个系统和服务器上获取/更新数据来服务的。在应用程序等待来自其他服务器的信息时,当前平台线程保持在空闲状态。这是对计算资源的浪费,也是实现高吞吐量应用程序的主要障碍。

1.3 反应式编程 (Reactive Programming) 的问题

反应式编程解决了平台线程等待其他系统响应的问题。异步 API 不等待响应,而是通过回调工作。每当线程调用异步 API 时,平台线程都会返回到池中,直到响应从远程系统或数据库返回。稍后,当响应到达时,JVM 将从池中分配另一个线程来处理响应,依此类推。这样,多个线程参与处理单个异步请求。

在异步编程中,延迟被消除了,但由于硬件限制,平台线程的数量仍然有限,因此我们对可扩展性有限制。另一个大问题是这样的异步程序在不同的线程中执行,因此很难调试或分析它们。

此外,我们必须采用一种新的编程风格,远离典型的循环和条件语句。新的 lambda 样式语法使得理解现有代码和编写程序变得困难,因为我们现在必须将程序分解为多个可以独立和异步运行的代码块。

所以我们可以说,虚拟线程还通过适应传统语法来提高代码质量,同时具有反应式编程的好处。

1.4 虚拟线程的前景

与传统线程类似,虚拟线程也是 java.lang.Thread 的一个实例,它在底层 OS 线程上运行其代码,但它不会在代码的整个生命周期内阻塞 OS 线程。保持操作系统线程空闲意味着许多虚拟线程可以在同一个操作系统线程上运行它们的 Java 代码,从而有效地共享它。

值得一提的是,我们可以在一个应用程序中创建非常多的虚拟线程(数百万),而不依赖于平台线程的数量。这些虚拟线程由 JVM 管理,因此它们也不会增加额外的上下文切换开销,因为它们作为普通 Java 对象存储在 RAM 中。

与传统线程类似,应用程序的代码在请求的整个持续时间内都在虚拟线程中运行(以每个请求线程的方式),但虚拟线程仅在 CPU 上执行计算时才消耗操作系统线程。它们在等待或休眠时不会阻塞 OS 线程。

虚拟线程有助于实现与具有相同硬件配置的异步 API 相同的高可扩展性和吞吐量,而不会增加语法复杂性。

虚拟线程最适合执行大部分时间都处于阻塞状态的代码,例如等待数据到达网络套接字或等待队列中的元素。

2. 平台线程和虚拟线程的区别

虚拟线程始终是守护线程。 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

3. 比较平台线程和虚拟线程的性能

让我们了解两种线程在执行相同的代码时的区别。

我们有一个非常简单的任务,在控制台中打印一条消息之前等待 1 秒。

final AtomicInteger atomicInteger = new AtomicInteger();Runnable runnable = () -> {try {Thread.sleep(Duration.ofSeconds(1));} catch(Exception e) {System.out.println(e);}System.out.println("Work Done - " + atomicInteger.incrementAndGet());
};

现在我们将用这个 Runnable 创建 10,000 个线程,并用虚拟线程和平台线程执行它们,来比较两者的性能。我们将使用 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 秒

到今天为止,虚拟线程是一个预览的 API,默认情况下是关闭的。使用 $ java --source 19 --enable-preview Main.java 运行代码。

接下来,我们将 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 代码没有任何变化。

4. 如何创建虚拟线程

4.1 使用 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");
});

4.2 使用 Thread.Builder

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

这里的 Thread.ofVirtual().start(runnable) 等价于 Thread.startVirtualThread(runnable)

Runnable runnable = () -> System.out.println("Virtual Thread");
Thread virtualThread = Thread.ofVirtual().start(runnable);

我们可以使用 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);

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

Thread.Builder builder = Thread.ofPlatform().name("Platform-Thread");Thread t1 = builder.start(() -> {...});
Thread t2 = builder.start(() -> {...});

4.3 使用 Executors.newVirtualThreadPerTaskExecutor()

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

在以下示例中,我们将提交 10,000 个任务并等待所有任务完成。代码将创建 10,000 个虚拟线程来完成这 10,000 个任务。

请注意,以下语法是结构化并发的一部分,这是 Project Loom 中提出的另一个新特性。我们将在单独的帖子中讨论它。

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

5. 最佳实践

5.1 不要建虚拟线程池

Java 线程池旨在避免创建新操作系统线程的开销,因为创建它们是一项昂贵的操作。但是创建虚拟线程并不昂贵,因此永远不需要将它们池化。建议每次需要时创建一个新的虚拟线程。

请注意,使用虚拟线程后,我们的应用程序可能能够处理数百万个线程,但其他系统或平台一次只能处理几个请求。例如,我们可以只有几个数据库连接或与其他服务器的网络连接。

在这些情况下,也不要使用线程池。相反,使用信号量来确保只有指定数量的线程正在访问该资源。

private static final Semaphore SEMAPHORE = new Semaphore(50);SEMAPHORE.acquire();try {// 信号量被控制在 50 来访问请求// 访问数据库或资源
} finally {SEMAPHORE.release();
}

5.2 避免使用线程局部变量 (ThreadLocal)

虚拟线程支持线程局部行为的方式与平台线程相同,但由于虚拟线程可以创建数百万个,因此只有在仔细考虑后才能使用线程局部变量。

例如,如果我们在应用程序中扩展一百万个虚拟线程,那么将有一百万个 ThreadLocal 实例以及它们所引用的数据。如此大量的实例会给内存带来很大的负担,应该避免。

在以后的 Java 版本中或许可以使用 Extent-Local 变量。

5.3 使用 ReentrantLock 而不是同步块

有两种特定场景,虚拟线程可以阻塞平台线程(称为 OS 线程的固定)。

  • 当它在同步块或同步方法内执行代码时
  • 当它执行本地方法或外部函数时

这种同步块不会使应用程序出错,但它会限制应用程序的可扩展性,类似于平台线程。

作为最佳实践,如果一个方法使用非常频繁并且它使用同步块,则考虑将其替换为 ReentrantLock 机制。

所以不要像这样使用同步块:

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

建议不需要替换不经常使用的同步块和方法,例如,仅在启动时执行。

6. 结论

长期以来,传统的 Java 线程一直很好用。随着微服务领域对可扩展性和高吞吐量的需求不断增长,虚拟线程将被证明是 Java 历史上的一个里程碑特性。

使用虚拟线程,一个程序可以用少量的物理内存和计算资源处理数百万个线程,这是传统平台线程无法做到的。当与结构化并发相结合时,它还将导致编写更好的程序。

Java 19 虚拟线程相关推荐

  1. Java VirtualThread 虚拟线程的个人粗浅理解

    先随便看个VirtualThread的相关资料理解虚拟线程和平台线程的概念或者看这个也行Java19 正式 GA!看虚拟线程如何大幅提高系统吞吐量 先说结论:虚拟线程依赖于ForkJoinPool() ...

  2. 洗料系列-编程语言专题-Java 19【线程×,协程√】

    一.进程.线程 表1. 进程.线程.协程一览 概念  调度 创建和切换代价 组成 备注 进程 操作系统 极高 资源分配的最小单位 线程 操作系统 高 进程的组成部分 CPU调度和执行的最小单位 协程 ...

  3. JDK 19 / Java 19 正式GA

    JDK 19 / Java 19 已正式发布. 新版本总共包含 7 个新的 JEP: 405: Record Patterns (Preview) 422: Linux/RISC-V Port 424 ...

  4. Java高并发革命!JDK19新特性——虚拟线程(Virtual Threads)

    介绍 虚拟线程具有和 Go 语言的 goroutines 和 Erlang 语言的进程类似的实现方式,它们是用户模式(user-mode)线程的一种形式. 在过去 Java 中常常使用线程池来进行平台 ...

  5. Java虚拟线程:一种低成本、轻量级、用户态的Java线程

    点击上方"朱小厮的博客",选择"设为星标" 后台回复"书",获取 后台回复"k8s",可领取k8s资料 OpenJDK在 ...

  6. java new一个线程执行完后会自动销毁吗_Java基础总结,超级全的面试题

    1. static关键字是什么意思?Java 中是否可以覆盖(override)一个 private 或者是 static 的方法?是否可以在 static 环境中访问非static 变量? stat ...

  7. JDK 19 / Java 19 正式发布

    近日,甲骨文公司正式发布Java 19,这是备受欢迎的编程语言和开发平台推出的全新版本.Java19 (Oracle JDK 19) 在性能.稳定性和安全性方面进行了数千种改进,包括进一步优化平台的增 ...

  8. Java:JDK 19——Java 19的新特性

    ​ Java 开发工具包19(JDK 19)将于今年 9 月发布,现已功能完善,目前处于第二阶段.该版本的七项功能包括结构化并发.记录模式.外部函数和内存 API 的预览,以及对开源 Linux/RI ...

  9. JDK 19:针对Java 19的功能

    虚拟线程.switch表达式的模式匹配.vector API和Linux/RISC-V端口将用于Java 19,这是标准Java的非LTS版本,将于9月发布. 定于今年9月发布的Java Develo ...

最新文章

  1. R语言可视化分面图、假设检验、单变量分组多水平t检验并指定参考水平、可视化单变量分组多水平分面箱图(faceting boxplot)并添加显著性水平、指定显著性参考水平
  2. protobuf与protoc-gen-go
  3. CTSC 2018 游记
  4. raspberry树莓派NFS搭建
  5. 机器学习(一)线性回归
  6. UVA 12298——Super Poker II
  7. linux 系统显示很大,在Linux中可视化显示内存占用情况的方法
  8. Windows学习总结(9)——Windows系统常用的网络控制指令
  9. 小帅小胖智能机器人价格_小胖机器人CEO魏然:机器人+教育,是未来教育大势所趋...
  10. c++ -- 重载、重写(覆盖)和隐藏的区别
  11. FireFox 插件SQLite Manager 学习
  12. 零基础语法入门第九/十讲 使用疑问词进行提问和回答
  13. python漂亮界面 数独游戏源代码_Python编写的超帅数独可视化解题器
  14. 手机开机启动慢是什么原因_手机开机的速度太慢怎么办
  15. android webview goback 刷新,解决webview调用goBack()返回上一页自动刷新闪白的情况
  16. ORACLE日期函数浅谈
  17. Android快速转战Kotlin教程,面试必备
  18. 手游代理怎么做?看看这些就知道了
  19. 国外安全网站、社区论坛、博客、公司、在线工具等整合收集
  20. AndServer+Service 打造 Android 服务器实现 so 文件调用

热门文章

  1. 餐饮管理系统 VC MFC
  2. 半监督学习(一)--(GMM/EM/Based on Graph)
  3. GDC翻译:Far Cry 5 的程序化世界生成(第三部分:7-生态工具(Biome Tool))
  4. typora公式大全
  5. 关于微信小程序打开/保存excel、pdf、word文档
  6. POI-对于WORD的操作
  7. NYOJ--888-取石子(九) (反nim博弈)
  8. 又一家光器件企业上市:珠海光库科技
  9. delivery route配送路线
  10. Python 之谷歌瓦片地图影像批量下载