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)。
有 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% 忙碌的。
因此,所有工作都立即有效地完成,因为所有百万线程同时进行 12 秒的小睡,而平台/内核线程以五个一组的方式连续小睡。我们在上面的屏幕截图中看到,数百万个任务的工作是如何在几秒钟内一次性完成的,而平台/内核线程做的工作量相同,但分散了几天。
请注意,这种显着的性能提升只发生在您的任务经常被阻止时。如果使用 CPU 密集型任务,例如视频编码,那么您应该使用平台/内核线程而不是虚拟线程。大多数业务应用程序会遇到很多阻塞,例如等待对文件系统、数据库、其他外部服务或网络的调用以访问远程服务。虚拟线程在这种经常阻塞的工作负载中大放异彩。