0

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

我想知道在Project Loom下这样做。

  • Project Loom技术下使用虚拟线程(纤Thread.sleep程),我们可以同样使用吗?
  • 休眠虚拟线程与休眠平台/内核线程有什么不同或值得注意的吗?

为了自学,我观看了 2020 年末与 Oracle 的 Ron Pressler 一起展示 Project Loom 技术的视频(此处此处)。虽然很有启发性,但我不记得他解决过休眠线程的问题。

4

3 回答 3

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

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

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

你继续问,

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

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

  • 虚拟线程是一个线程——在代码中、在运行时、在调试器中和在分析器中。

  • 无需更改语言

(强调补充。)

于 2020-12-17T01:38:17.220 回答
3

源码,当你调用sleep(...)一个虚拟线程时,是由JVM的虚拟线程调度器来处理的;即不直接进行系统调用并且不阻塞本机线程。

所以:

在 Project Loom 技术下使用虚拟线程(纤Thread.sleep程),我们可以同样使用吗?

是的。

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

休眠虚拟线程的处理方式与您期望虚拟线程的行为相同。性能将与内核线程不同,但行为被设计为透明的应用程序代码......这不会对线程调度程序行为做出无根据的假设。

无论如何,Thread.sleep(...)Loom 中的 javadocs 目前没有提到内核线程和虚拟线程之间的任何区别。

于 2020-12-17T01:39:24.430 回答
2

John Bollinger答案和 Stephen C的答案都是正确且内容丰富的。我想我会添加一个代码示例来显示:

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

基准代码

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

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

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

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

  • ExecutorService executorService = Executors.newFixedThreadPool( 5 )
    常规线程池,在这台 Mac mini(2018 年)上使用 6 个真实内核中的 5 个(无超线程),配备 3 GHz Intel Core i5 处理器和 32 GB RAM。
  • ExecutorService executorService = Executors.newVirtualThreadExecutor()
    由 Project Loom 在这个早期访问 Java 16 的特殊版本中提供的新虚拟线程(纤程)支持的执行器服务。
package work.basil.example;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TooFast
{
    public static void main ( String[] args )
    {
        TooFast app = new TooFast();
        app.demo();
    }

    private void demo ( )
    {
        System.out.println( "INFO - starting `demo`. " + Instant.now() );

        long start = System.nanoTime();
        try (
                // 5 of 6 real cores, no hyper-threading.
                ExecutorService executorService = Executors.newFixedThreadPool( 5 ) ;
                //ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
        )
        {
            Duration sleep = Duration.ofSeconds( 12 );
            int limit = 100;
            for ( int i = 0 ; i < limit ; i++ )
            {
                executorService.submit(
                        new Runnable()
                        {
                            @Override
                            public void run ( )
                            {
                                try {Thread.sleep( sleep );} catch ( InterruptedException e ) {e.printStackTrace();}
                                long x = ( System.nanoTime() - 42 );
                                System.out.println( "x = " + x );
                            }
                        }
                );
            }
        }
        // With Project Loom, the flow-of-control  blocks here until all submitted tasks have finished.
        Duration demoElapsed = Duration.ofNanos( System.nanoTime() - start );

        System.out.println( "INFO - demo took " + demoElapsed + " ending at " + Instant.now() );
    }
}

结果

结果令人吃惊。

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

其次,虚拟线程只需几秒钟即可完成所有任务,而传统线程只需几分钟、几小时或几天。

有 100 个任务:

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

有 1,000 个任务:

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

条形图显示了 12 秒的虚拟线程和 2400 秒的常规线程的经过时间,在 1,000 个任务期间经过

有 1,000,000 个任务:

  • 传统线程需要……嗯,几天。
    (我实际上并没有等待。我之前在此代码的早期版本中经历了 50 万次循环的 29 小时运行。)
  • 虚拟线程需要 28 秒 (PT28.043056938S)。
    (如果我们减去 12 秒的休眠时间,100 万个线程在剩余的 16 秒内执行所有工作,即每秒大约有 62,500 个线程任务被立即执行。)

结论

使用常规线程,我们可以看到控制台上突然出现几行重复爆发。所以我们可以看到平台/内核线程实际上是如何在核心上被阻塞的,因为它们等待它们的 12 秒Thread.sleep到期。然后所有五个线程在大约同一时刻唤醒,所有线程都在大约同一时刻开始,每 12 秒一次,同时进行数学运算并写入控制台。这种行为得到了证实,因为我们在Activity Monitor应用程序中看到 CPU 内核的使用很少。

顺便说一句:我假设主机操作系统注意到我们的 Java 线程实际上正忙于无所事事,然后使用它的 CPU 调度程序在阻塞时暂停我们的 Java 线程,让其他进程(如其他应用程序)使用 CPU 内核。但如果是这样,这对我们的 JVM 来说是透明的。从 JVM 的角度来看,休眠的 Java 线程在整个小睡期间都在占用 CPU。

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

有关更多信息,请参阅 Oracle Project Loom 的 Ron Pressler 最近(2020 年末)的任何演讲,以及他 2020 年 5 月的论文State of Loom。这种快速交换阻塞虚拟线程的行为非常有效,以至于 CPU 可以一直保持忙碌。我们可以在Activity Monitor应用程序中确认这种效果。这是使用虚拟线程运行百万个任务的Activity Monitor的屏幕截图。请注意,在所有百万线程完成 12 秒的小睡之后,CPU 内核实际上是 100% 忙碌的。

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

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

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

于 2020-12-24T04:47:11.560 回答