“Thread.sleep”与 Project Loom for Java 中的虚拟线程(纤程)不同吗

Bas*_*que 4 java fibers thread-sleep project-loom

Thread.sleep我在试验或演示 Java 代码的并发性时使用。通过睡觉,我假装正在进行一些需要一些时间的处理工作。

我想知道在Project Loom下做这件事。

为了自我教育,我观看了一些 2020 年末 Oracle 的 Ron Pressler 介绍 Project Loom 技术的视频(此处此处)。虽然很有启发性,但我不记得他提到过休眠线程的问题。

Joh*_*ger 6

\n
    \n
  • 在带有虚拟线程(纤程)的Project Loom技术下,我们可以以同样的方式使用Thread.sleep吗?
  • \n
\n
\n

看来是这样。我参考了 OpenJDK wiki 上解决Loom 中的阻塞操作的页面。它列出了Thread.sleep()对虚拟线程友好的操作,这意味着

\n
\n

当未固定时,它们会在操作阻塞时释放底层承载线程以执行其他工作。

\n
\n

你继续问,

\n
\n
    \n
  • 休眠虚拟线程与休眠平台/内核线程有什么不同或值得注意的地方吗?
  • \n
\n
\n

文档很少,并且尚不清楚实际存在的任何差异是否是故意的。尽管如此,我倾向于认为休眠虚拟线程的目标是使其语义尽可能接近休眠普通线程的语义。我怀疑足够聪明的程序会有办法区分,但如果有任何差异上升到“值得注意”的水平,那么我预计它们将被视为错误。我的这一点部分基于推理,但我也请您参考Loom 的状态java.net 上的

\n
\n
    \n
  • 虚拟线程是代码中、运行时、调试器和分析器中的线程 \xe2\x80\x94。
  • \n
\n
\n

\n
\n
    \n
  • 无需更改语言
  • \n
\n
\n

(强调是添加的。)

\n


Bas*_*que 5

约翰·博林格 (John Bollinger) 的回答和 C 的答案都是正确且内容丰富的。我想我应该添加一个代码示例来显示:

\n
    \n
  • 虚拟线程和平台/内核线程如何尊重Thread.sleep.
  • \n
  • Project Loom 技术可以实现惊人的性能提升。
  • \n
\n

基准测试代码

\n

让我们简单地写一个循环。在每个循环中,我们实例化 aRunnable来执行任务,并将该任务提交给执行器服务。我们的任务是:做一些简单的数学运算,从long返回的值中减去System.nanoTime减去。最后,我们将该数字打印到控制台。

\n

但诀窍是之前,我们让执行该任务的线程休眠。由于每次都在最初的 12 秒内休眠,因此在至少 12 秒的死区时间之后,我们应该在控制台上看不到任何内容。

\n

然后提交的任务执行它们的工作。

\n

我们通过两种方式运行它,即启用/禁用一对注释掉的行。

\n
    \n
  • ExecutorService executorService = Executors.newFixedThreadPool( 5 )
    传统线程池,在配备 3 GHz Intel Core i5 处理器和 32 GB RAM 的 Mac mini(2018)上使用 6 个真实核心中的 5 个(无超线程)。
  • \n
  • ExecutorService executorService = Executors.newVirtualThreadExecutor()
    执行器服务由 Project Loom 在早期访问 Java 16 的特殊构建中提供的新虚拟线程(纤程)支持。
  • \n
\n
package work.basil.example;\n\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class TooFast\n{\n    public static void main ( String[] args )\n    {\n        TooFast app = new TooFast();\n        app.demo();\n    }\n\n    private void demo ( )\n    {\n        System.out.println( "INFO - starting `demo`. " + Instant.now() );\n\n        long start = System.nanoTime();\n        try (\n                // 5 of 6 real cores, no hyper-threading.\n                ExecutorService executorService = Executors.newFixedThreadPool( 5 ) ;\n                //ExecutorService executorService = Executors.newVirtualThreadExecutor() ;\n        )\n        {\n            Duration sleep = Duration.ofSeconds( 12 );\n            int limit = 100;\n            for ( int i = 0 ; i < limit ; i++ )\n            {\n                executorService.submit(\n                        new Runnable()\n                        {\n                            @Override\n                            public void run ( )\n                            {\n                                try {Thread.sleep( sleep );} catch ( InterruptedException e ) {e.printStackTrace();}\n                                long x = ( System.nanoTime() - 42 );\n                                System.out.println( "x = " + x );\n                            }\n                        }\n                );\n            }\n        }\n        // With Project Loom, the flow-of-control  blocks here until all submitted tasks have finished.\n        Duration demoElapsed = Duration.ofNanos( System.nanoTime() - start );\n\n        System.out.println( "INFO - demo took " + demoElapsed + " ending at " + Instant.now() );\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

结果

\n

结果令人震惊。

\n

首先,在这两种情况下,我们都看到控制台活动之前有超过 12 秒的延迟。所以我们知道Thread.sleep是由平台/内核线程和虚拟线程真正执行的。

\n

其次,虚拟线程完成所有工作只需几秒钟即可

\n

有 100 个任务:

\n
    \n
  • 传统线程需要 4 分钟 (PT4M0.079402569S)。
  • \n
  • 虚拟线程只需要 12 秒多一点 (PT12.087101159S)。
  • \n
\n

有 1,000 个任务:

\n
    \n
  • 传统线程需要 40 分钟 (PT40M0.667724055S)。
    (这是有道理的:1,000 * 12 / 5 / 60 = 40)
  • \n
  • 虚拟线程需要 12 秒 (PT12.177761325S)。
  • \n
\n

条形图显示 1,000 个任务期间虚拟线程的运行时间为 12 秒,传统线程的运行时间为 2400 秒

\n

拥有 1,000,000 个任务:

\n
    \n
  • 传统线程需要\xe2\x80\xa6 好几天。
    (我实际上并没有等待。我之前曾在此代码的早期版本中经历过 29 小时运行 50 万个循环。)
  • \n
  • 虚拟线程需要 28 秒 (PT28.043056938S)。
    (如果我们减去 12 秒的睡眠死区时间,则在剩余 16 秒内执行所有工作的 100 万个线程相当于每秒约立即执行 62,500 个线程任务。)
  • \n
\n

结论

\n

使用传统线程,我们可以看到控制台上突然出现几行重复突发。Thread.sleep因此,我们可以看到平台/内核线程在等待 12 秒到期时实际上是如何在核心上被阻塞的。然后,所有五个线程大约在同一时刻醒来,几乎在同一时刻(每 12 秒)启动一次,同时进行数学计算并写入控制台。由于我们在Activity Monitor应用程序中看到 CPU 核心的使用率很低,因此这一行为得到了证实。

\n

顺便说一句:我假设主机操作系统\xe2\x80\x99s 注意到我们的 Java 线程实际上正忙着什么都不做,然后使用其 CPU 调度程序在阻塞时挂起我们的 Java 线程,以让其他进程(例如其他应用程序)使用CPU 核心。但如果是这样,这对我们的 JVM 来说是透明的。从 JVM\xe2\x80\x99s 的角度来看,休眠的 Java 线程在整个午睡期间占用了 CPU。

\n

通过虚拟线程,我们看到了截然不同的行为。Project Loom 的设计使得当虚拟线程阻塞时,JVM 将该虚拟线程移出平台/内核线程,并放置另一个虚拟线程。这种 JVM 内的线程交换非常重要比交换平台/内核线程要便宜承载这些各种虚拟线程的平台/内核线程可以保持忙碌,而不是等待每个块通过。

\n

有关更多信息,请参阅 Oracle Loom 项目的 Ron Pressler 最近(2020 年末)的演讲以及他的 2020-05 论文State of Loom。这种快速交换阻塞虚拟线程的行为非常高效,以至于 CPU 可以一直保持忙碌状态。我们可以在Activity Monitor应用程序中确认此效果。这是活动监视器的屏幕截图这是使用虚拟线程运行数百万个任务的请注意,在所有 100 万个线程完成 12 秒的休眠后,CPU 核心几乎达到 100% 繁忙。

\n

活动监视器的屏幕截图显示,在调度一百万个任务和执行这些任务时,CPU 核心繁忙,但在休眠这百万个任务的 12 秒内几乎没有任何活动。

\n

因此,所有工作都立即有效地完成,因为所有数百万个线程同时进行 12 秒的小睡,而平台/内核线程则以 5 个为一组连续进行小睡。我们在上面的屏幕截图中看到,数百万个任务的工作如何在几秒钟内一次性完成,而平台/内核线程执行相同数量的工作,但将其分散在几天内。

\n

请注意,只有当您的任务经常被阻塞时,才会出现这种显着的性能提升。如果使用 CPU 密集型任务(例如视频编码),那么您应该使用平台/内核线程而不是虚拟线程。大多数业务应用程序都会遇到很多阻塞,例如等待对文件系统、数据库、其他外部服务或网络访问远程服务的调用。虚拟线程在这种经常阻塞的工作负载中大放异彩。

\n