提问者:小点点

究竟是什么让Java虚拟线程变得更好


我为Project Loom大肆宣传,但有一件事我不能完全理解。

大多数 Java 服务器使用具有一定线程限制(200、300 ..)的线程池,但是,您不受操作系统的限制来生成更多线程,我已经读到,使用 Linux 的特殊配置,您可以达到巨大的数量。

操作系统线程更昂贵,启动/停止速度较慢,必须处理上下文切换(按其数量放大),并且您依赖于操作系统,操作系统可能会拒绝为您提供更多线程。

话虽如此,虚拟线程也会消耗相似数量的内存(或者至少这是我所理解的)。使用Loom,我们可以获得尾部调用优化,这应该可以减少内存使用。此外,同步和线程上下文复制应该仍然是类似大小的问题。

事实上,您能够产生数百万个虚拟线程

public static void main(String[] args) {
    for (int i = 0; i < 1_000_000; i++) {
        Thread.startVirtualThread(() -> {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
}

上面的代码在 25k 左右中断,当我使用平台线程时出现 OOM 异常。

我的问题是,究竟是什么让这些线程如此轻巧,是什么阻止我们生成 100 万个平台线程并使用它们,只是上下文切换使常规线程如此“繁重”。

一个非常相似的问题

到目前为止我发现的东西:

    < li >上下文切换开销很大。一般来说,即使在理想情况下,操作系统知道线程将如何运行,它仍然必须给每个线程平等的执行机会,因为它们具有相同的优先级。如果我们产生10k操作系统线程,它将不得不在它们之间不断切换,在某些情况下,仅这项任务就可能占用高达80%的CPU时间,所以我们必须非常小心这些数字。对于虚拟线程,上下文切换由JVM完成,这使得它基本上是自由的 < li >便宜的启动/停止。当我们中断一个线程时,我们实际上是告诉任务,“杀死你正在运行的操作系统线程”。然而,举例来说,如果那个线程在一个线程池中,到我们请求的时候,线程可能被当前任务释放,然后给另一个任务,另一个任务可能得到中断信号。这使得中断过程相当复杂。虚拟线程只是堆中的对象,我们可以让GC在后台收集它们 < li >线程的硬上限(最多几万个),取决于操作系统处理它们的方式。操作系统无法针对特定的应用程序和编程语言进行微调,因此它必须在内存方面为最坏的情况做准备。它必须分配更多的内存来满足所有的需求。在进行所有这些工作的同时,it必须确保重要的操作系统进程仍在运行。使用VT,您只会受到廉价内存的限制 < li >执行事务的线程与执行视频处理的线程的行为非常不同,操作系统必须为最坏的情况做好准备,并以最佳方式适应这两种情况,这意味着在大多数情况下,我们只能获得次优的性能。由于VT是由Java本身产生和管理的,因此可以完全控制它们和特定于任务的优化,而不局限于操作系统 < li >可调整大小的堆栈。操作系统为线程提供了一个大堆栈,以适应所有使用情况,虚拟线程在堆空间中有一个可调整大小的堆栈,它会动态调整大小以适应问题,使其变小 < li >较小的元数据大小。如上所述,平台线程使用1MB,而虚拟线程需要200-300字节来存储其元数据

共3个答案

匿名用户

协程(即虚拟线程)的一大优点是它们可以生成高级别的并发性,而没有回调的缺点。

我先介绍一下利特尔定律:

concurrency = arrival_rate * latency

我们可以将其重写为:

arrival_rate = concurrency/latency

在稳定的系统中,到达率等于吞吐量。

throughput = concurrency/latency

要提高吞吐量,您有两个选项:

    < li >减少延迟;这通常非常困难,因为您对远程调用或磁盘请求所花费的时间几乎没有影响。 < li >提高并发性

对于常规线程,由于上下文切换开销,很难通过阻塞调用达到高并发级别。在某些情况下,请求可以异步发出(例如,NIO Epoll或Netty io _ uring绑定),但这时您需要处理回调和回调地狱。

使用虚拟线程,可以异步发出请求并停放虚拟线程并调度另一个虚拟线程。一旦收到响应,虚拟线程就会被重新调度,这是完全透明的。编程模型比使用经典线程和回调直观得多。

匿名用户

虚拟线程被包裹在平台线程上,所以你可能会认为它们是JVM提供的一种幻觉,整个想法是将线程的生命周期变成CPU绑定的操作。

究竟是什么让Java虚拟线程变得更好?

虚拟线程优势

  • 表现出与平台线程完全相同的行为。
  • 一次性的,可以扩展到数百万。
  • 比平台线程更轻量级。
  • 快速创建时间,与创建字符串对象一样快。
  • JVM对IO操作进行分隔延续,虚拟线程没有IO。
  • 还可以像以前一样拥有顺序代码,但更有效。
  • JVM给人一种虚拟线程的错觉,整个故事都在平台线程上。
  • 只是随着虚拟线程CPU内核的使用变得更加并发,虚拟线程和多核CPU与ComputableFutures的组合以并行化代码非常强大

虚拟线程使用注意事项

> < li>

不要使用monitor,即synchronized块,但是这将在JDK的新版本中修复,替代方法是使用带有try-final语句的“ReentrantLock”。

堆栈上的本机帧阻塞,JNI's。这非常罕见

控制每个堆栈的内存(减少线程区域设置,无深度递归)

尚未更新的监控工具,如调试器、JConsole、VisualVM等

平台线程与虚拟线程。平台线程在基于 IO 的任务和操作中将操作系统线程作为人质,仅限于线程池和操作系统线程中的适用线程数,默认情况下它们是非守护程序线程

虚拟线程是用JVM实现的,在CPU绑定操作中,关联到平台线程并将它们返回到线程池,在IO绑定操作完成后,将从线程池中调用一个新线程,因此在这种情况下没有人质。

第四级架构有更好的理解。

中央处理器

    < li >多核cpu多核,在CPU中执行操作。

操作系统

  • OS线程操作系统调度程序将cpu时间分配给参与的操作系统线程。

JVM

  • 平台线程通过两个任务操作完全包装在操作系统线程上
  • 虚拟线程在每个 CPU 绑定操作中与平台线程相关联,每个虚拟线程可以与多个平台线程关联为不同的时间。

带有执行器服务的虚拟线程

> 使用executer服务更有效,因为它与线程池相关联,并且仅限于与它相关联的可用线程,但是与虚拟线程相比,使用Executer服务和虚拟容器,我们不需要处理或管理相关联的线程池。

 try(ExecutorService service = Executors.newVirtualThreadPerTaskExecutor()) {
     service.submit(ExecutorServiceVirtualThread::taskOne);
     service.submit(ExecutorServiceVirtualThread::taskTwo);
 }

Executor服务在JDK 19中实现了Auto Closable接口,因此当与“try with resource”一起使用时,一旦到达“try”块的末尾,就会调用“close”api,或者主线程将等待所有提交的任务及其专用虚拟线程完成其生命周期,并关闭相关的线程池。

 ThreadFactory factory = Thread.ofVirtual().name("user thread-", 0).factory();
 try(ExecutorService service = Executors.newThreadPerTaskExecutor(factory)) {
     service.submit(ExecutorServiceThreadFactory::taskOne);
     service.submit(ExecutorServiceThreadFactory::taskTwo);
 }

执行器服务也可以使用虚拟线程工厂创建,只需将线程工厂与其构造函数参数一起放置即可。

可以受益于未来和完整未来等执行器服务的功能。

在JEP-425上查找更多信息

匿名用户

有时,人们必须构建能够处理大量并发客户端的系统。由于RAM消耗和上下文切换成本,本机线程不足以实现这一点。

虚拟线程让我们能够同时运行数百万个I/O任务,而无需改变我们的思维模式。

这就是Golang进入这个行业的原因(除了谷歌的支持)。Goroutines是一个与Java的虚拟线程非常相似的概念,它们解决了同样的问题。

还有其他方法可以实现虚拟线程的功能(例如NIO和相关的React模式)。然而,这需要使用扭曲你思维的消息循环和回调(这就是为什么这么多人讨厌JavaScript)。在它们之上有抽象层,让事情变得更容易,但它们也有成本。