【译】在Java中创建了100万个线程以后转载
在我们深入研究创建一百万个线程之前,让我们看看Java线程的历史。
自Java 1.0以来,Java线程一直存在,以便在Java应用程序中提供并发性。最初,它们被称为“绿色线程”,主要是完全由JVM管理的线程。这是因为在Java 1.0发布时,CPU大多是单核,当时的操作系统根本不支持内核/平台线程(稍后会详细介绍)。这是一个One to Many实现,其中一个实际线程可能会满足许多Java线程的需求。这些Java线程有自己的调用堆栈,并使用了大量内存。
但随着CPU和操作系统变得越来越先进,并支持内核/平台级线程,线程的Java实现也发生了变化。Java后来采用了一对一实现,其中Java线程实际上是内核/平台线程上的薄包装器。内核/平台线程由操作系统创建和管理,除了堆存储外,包裹在内核/平台线程上的每个Java线程可能会消耗超过1兆字节的内存,并且创建成本很高。为了消除此线程池,用于重用Java线程。
这有一个关于Java应用程序可以支持并发的线程数量的上限。引入了以下公式来计算理想的线程池大小。
Thread Pool Size = Number of CPU Cores + 1
这在Java中始终是一个限制,因为其他最新的编程语言,如Go(goroutines)、Akka(actors)和Erlang((processes)具有不同的并发编程模型。
为了解决这一限制,Project Loom的诞生是为了支持Java中的“虚拟线程”,这些线程比现有的内核/平台包装线程更轻。现在,这些虚拟线程将再次由JVM管理,而不是由类似于Java 1.0绿色线程的底层操作系统管理。然而,绿色线程和虚拟线程之间的显著区别在于,虚拟线程可以有一个动态调用堆栈,可以增长和缩小,而绿色线程可以有固定的调用堆栈,消耗内存。
为了实现虚拟线程,Project Loom在JVM中引入了3个新概念。
- 调度器—这是一个ForkJoin池,其大小通常等于CPU中的核心数量。
- 载体线程—载体线程,指的是负责执行虚拟线程中任务的平台线程,或者说运行虚拟线程的平台线程称为它的载体线程。
- Continuation切换——这类似于运行,生成调用,其中虚拟线程可以根据JVM所做的在运行和空闲之间切换。
例如:-当虚拟线程执行数据库查询或HTTP请求等阻止调用时,它可能会生成,直到它收到响应,以便其他虚拟线程可以执行。
因此,让我们看看虚拟线程的运行情况,并将其与平台/内核线程进行比较。
Java 19发布了Project Loom的预览版,这是我用来进行这个演示的。最初,我运行了一个应用程序,试图创建100万个平台/内核线程。
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(1_000_000);
for (int i=0;i<1_000_000;i++) {
Thread normalThread = new Thread(() -> {
System.out.println("Hello, World from Regular Thread : " + Thread.currentThread().getName());
number.incrementAndGet();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
normalThread.start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("Time Taken = "+(end - start));
System.out.println("Number = "+number.get());
这导致JVM中的线程计数为113个,在我的Macbook Pro M1 pro CPU笔记本电脑中完成了超过10万毫秒(1.5分钟),内存为16千兆字节。
然后,我使用虚拟线程测试了一个类似的代码。在Java 19中编译和运行虚拟线程时,我必须使用-启用预览标志。
AtomicInteger number = new AtomicInteger(0);
CountDownLatch countDownLatch = new CountDownLatch(1_000_000);
long start = System.currentTimeMillis();
for (int i=0;i<1_000_000;i++) {
Thread.startVirtualThread(() -> {
System.out.println("Hello, World from Virtual Thread");
number.incrementAndGet();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("Time Taken = "+(end - start));
System.out.println("Number = "+number.get());
使用较少的线程(46个线程)在相同配置中运行的相同配置中仅运行13k毫秒(13秒)。
作为控件,我运行了另一个没有任何线程的Java应用程序,但只是睡觉来检查大约22个线程的默认数量。
这个演示清楚地表明,虚拟线程不会将一映射到内核/平台线程,在某些情况下,由于它能够使用载体线程池来执行其任务,可以更快地执行。这两个任务都有I/O操作和空闲时间。
结论
这显然并不意味着可以盲目地将所有现有线程替换为虚拟线程。然而,这开启了新的思维方式,即在Java中再次可以为每个任务创建虚拟线程。这意味着使用虚拟线程的Web服务器可以根据请求创建虚拟线程,而不必担心JVM内存耗尽。
这为在Java中实现并发开辟了新的途径,我很高兴看到它可能在Java 20或更高版本中全面发布。
原文作者:Shazin Sadakath